@adhdev/daemon-core 0.9.82-rc.8 → 0.9.82-rc.81
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/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +10 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +5074 -1177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5038 -1163
- 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 +60 -0
- package/dist/mesh/mesh-events.d.ts +29 -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-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +119 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/repo-mesh-types.d.ts +39 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +1 -0
- package/src/cli-adapters/provider-cli-adapter.ts +91 -3
- 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 +20 -10
- package/src/commands/chat-commands.ts +310 -12
- package/src/commands/cli-manager.ts +101 -0
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2435 -414
- package/src/config/chat-history.ts +9 -7
- package/src/config/mesh-config.ts +244 -1
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +3 -3
- package/src/git/git-status.ts +97 -6
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +11 -0
- package/src/index.ts +31 -5
- 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 +27 -7
- package/src/mesh/mesh-active-work.ts +243 -0
- package/src/mesh/mesh-events.ts +398 -46
- 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-work-queue.ts +199 -137
- package/src/mesh/refine-config.ts +306 -0
- package/src/providers/chat-message-normalization.ts +3 -1
- package/src/providers/cli-provider-instance.ts +91 -13
- 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 +43 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
package/src/commands/router.ts
CHANGED
|
@@ -38,13 +38,25 @@ 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, 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 {
|
|
45
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
46
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
47
|
+
loadMeshRefineConfig,
|
|
48
|
+
resolveMeshRefineValidationPlan,
|
|
49
|
+
suggestMeshRefineConfig,
|
|
50
|
+
validateMeshRefineConfig,
|
|
51
|
+
type MeshRefineValidationCommandPlan,
|
|
52
|
+
} from '../mesh/refine-config.js';
|
|
42
53
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
43
54
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
44
55
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
56
|
+
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
45
57
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
46
58
|
import { homedir } from 'os';
|
|
47
|
-
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
59
|
+
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
48
60
|
import * as fs from 'fs';
|
|
49
61
|
|
|
50
62
|
type ReleaseChannel = 'stable' | 'preview';
|
|
@@ -114,14 +126,95 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
114
126
|
return undefined;
|
|
115
127
|
}
|
|
116
128
|
|
|
117
|
-
function
|
|
129
|
+
function summarizeRepoMeshDebugGit(git: unknown): Record<string, unknown> | null {
|
|
130
|
+
const record = readObjectRecord(git);
|
|
131
|
+
if (!Object.keys(record).length) return null;
|
|
132
|
+
const submodules = Array.isArray(record.submodules)
|
|
133
|
+
? record.submodules.map((entry: any) => ({
|
|
134
|
+
path: readStringValue(entry?.path) ?? null,
|
|
135
|
+
commit: readStringValue(entry?.commit)?.slice(0, 12) ?? null,
|
|
136
|
+
dirty: readBooleanValue(entry?.dirty) ?? false,
|
|
137
|
+
outOfSync: readBooleanValue(entry?.outOfSync, entry?.out_of_sync) ?? false,
|
|
138
|
+
}))
|
|
139
|
+
: [];
|
|
140
|
+
return {
|
|
141
|
+
isGitRepo: readBooleanValue(record.isGitRepo),
|
|
142
|
+
workspace: readStringValue(record.workspace) ?? null,
|
|
143
|
+
repoRoot: readStringValue(record.repoRoot, record.repo_root) ?? null,
|
|
144
|
+
branch: readStringValue(record.branch) ?? null,
|
|
145
|
+
upstream: readStringValue(record.upstream) ?? null,
|
|
146
|
+
upstreamStatus: readStringValue(record.upstreamStatus, record.upstream_status) ?? null,
|
|
147
|
+
headCommit: readStringValue(record.headCommit, record.head_commit)?.slice(0, 12) ?? null,
|
|
148
|
+
ahead: readNumberValue(record.ahead) ?? null,
|
|
149
|
+
behind: readNumberValue(record.behind) ?? null,
|
|
150
|
+
dirtyCounts: {
|
|
151
|
+
staged: readNumberValue(record.staged) ?? 0,
|
|
152
|
+
modified: readNumberValue(record.modified) ?? 0,
|
|
153
|
+
untracked: readNumberValue(record.untracked) ?? 0,
|
|
154
|
+
deleted: readNumberValue(record.deleted) ?? 0,
|
|
155
|
+
renamed: readNumberValue(record.renamed) ?? 0,
|
|
156
|
+
},
|
|
157
|
+
lastCheckedAt: readNumberValue(record.lastCheckedAt, record.last_checked_at) ?? null,
|
|
158
|
+
submoduleCount: submodules.length,
|
|
159
|
+
submodules,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
164
|
+
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
165
|
+
return {
|
|
166
|
+
success: status?.success,
|
|
167
|
+
meshId: readStringValue(status?.meshId, status?.mesh_id) ?? null,
|
|
168
|
+
refreshedAt: readStringValue(status?.refreshedAt, status?.refreshed_at) ?? null,
|
|
169
|
+
sourceOfTruth: status?.sourceOfTruth ?? null,
|
|
170
|
+
branchConvergenceSummary: status?.branchConvergenceSummary ?? status?.branch_convergence_summary ?? null,
|
|
171
|
+
nodeCount: nodes.length,
|
|
172
|
+
nodes: nodes.map((node: any) => ({
|
|
173
|
+
nodeId: readStringValue(node?.nodeId, node?.id) ?? null,
|
|
174
|
+
daemonId: readStringValue(node?.daemonId, node?.daemon_id) ?? null,
|
|
175
|
+
workspace: readStringValue(node?.workspace, node?.git?.workspace) ?? null,
|
|
176
|
+
health: readStringValue(node?.health) ?? null,
|
|
177
|
+
machineStatus: readStringValue(node?.machineStatus, node?.machine_status) ?? null,
|
|
178
|
+
connection: node?.connection && typeof node.connection === 'object' ? {
|
|
179
|
+
state: readStringValue(node.connection.state) ?? null,
|
|
180
|
+
transport: readStringValue(node.connection.transport) ?? null,
|
|
181
|
+
source: readStringValue(node.connection.source) ?? null,
|
|
182
|
+
reported: readBooleanValue(node.connection.reported) ?? null,
|
|
183
|
+
} : null,
|
|
184
|
+
gitProbePending: node?.gitProbePending === true,
|
|
185
|
+
launchReady: node?.launchReady === true,
|
|
186
|
+
git: summarizeRepoMeshDebugGit(node?.git),
|
|
187
|
+
branchConvergence: node?.branchConvergence ?? node?.branch_convergence ?? null,
|
|
188
|
+
})),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
193
|
+
try {
|
|
194
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
195
|
+
} catch {
|
|
196
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${event}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
|
|
201
|
+
const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : '';
|
|
202
|
+
const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : '';
|
|
203
|
+
if (!normalizedPath) return undefined;
|
|
204
|
+
if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath;
|
|
205
|
+
if (!normalizedRoot) return undefined;
|
|
206
|
+
return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
|
|
118
210
|
if (!Array.isArray(value)) return undefined;
|
|
119
211
|
const submodules = value
|
|
120
212
|
.map(entry => {
|
|
121
213
|
const submodule = readObjectRecord(entry);
|
|
122
214
|
const path = readStringValue(submodule.path);
|
|
123
215
|
const commit = readStringValue(submodule.commit);
|
|
124
|
-
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
216
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
217
|
+
?? joinRepoPath(parentRepoRoot, path);
|
|
125
218
|
if (!path || !commit || !repoPath) return null;
|
|
126
219
|
return {
|
|
127
220
|
path,
|
|
@@ -137,60 +230,23 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
|
137
230
|
return submodules.length > 0 ? submodules : undefined;
|
|
138
231
|
}
|
|
139
232
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const submodules = readGitSubmodules(cachedGit.submodules);
|
|
152
|
-
return {
|
|
153
|
-
workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
|
|
154
|
-
repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
|
|
155
|
-
isGitRepo,
|
|
156
|
-
branch: readStringValue(cachedGit.branch) ?? null,
|
|
157
|
-
headCommit: readStringValue(cachedGit.headCommit) ?? null,
|
|
158
|
-
headMessage: readStringValue(cachedGit.headMessage) ?? null,
|
|
159
|
-
upstream: readStringValue(cachedGit.upstream) ?? null,
|
|
160
|
-
ahead: readNumberValue(cachedGit.ahead) ?? 0,
|
|
161
|
-
behind: readNumberValue(cachedGit.behind) ?? 0,
|
|
162
|
-
staged: readNumberValue(cachedGit.staged) ?? 0,
|
|
163
|
-
modified: readNumberValue(cachedGit.modified) ?? 0,
|
|
164
|
-
untracked: readNumberValue(cachedGit.untracked) ?? 0,
|
|
165
|
-
deleted: readNumberValue(cachedGit.deleted) ?? 0,
|
|
166
|
-
renamed: readNumberValue(cachedGit.renamed) ?? 0,
|
|
167
|
-
hasConflicts,
|
|
168
|
-
conflictFiles,
|
|
169
|
-
stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
|
|
170
|
-
lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
|
|
171
|
-
...(submodules ? { submodules } : {}),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
233
|
+
function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
|
|
234
|
+
const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
|
|
235
|
+
if (explicit) return explicit;
|
|
236
|
+
const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
|
|
237
|
+
const workspaceName = workspace ? pathBasename(workspace) : undefined;
|
|
238
|
+
const host = readStringValue(node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
|
|
239
|
+
const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
|
|
240
|
+
const parts = [workspaceName, host, provider].filter(Boolean);
|
|
241
|
+
if (parts.length > 0) return parts.join(' · ');
|
|
242
|
+
return nodeId || 'unidentified mesh node';
|
|
243
|
+
}
|
|
175
244
|
|
|
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
|
-
: {};
|
|
245
|
+
function normalizeInlineMeshGitStatus(
|
|
246
|
+
status: Record<string, unknown>,
|
|
247
|
+
node: any,
|
|
248
|
+
options?: { lastCheckedAt?: number },
|
|
249
|
+
): Record<string, unknown> | undefined {
|
|
194
250
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
251
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
252
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -198,15 +254,20 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
198
254
|
: [];
|
|
199
255
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
256
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
-
const
|
|
257
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
258
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
202
259
|
return {
|
|
203
260
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
-
repoRoot:
|
|
261
|
+
repoRoot: repoRoot ?? null,
|
|
205
262
|
isGitRepo,
|
|
206
263
|
branch: readStringValue(status.branch) ?? null,
|
|
207
264
|
headCommit: readStringValue(status.headCommit) ?? null,
|
|
208
265
|
headMessage: readStringValue(status.headMessage) ?? null,
|
|
209
266
|
upstream: readStringValue(status.upstream) ?? null,
|
|
267
|
+
upstreamStatus: readStringValue(status.upstreamStatus, status.upstream_status)
|
|
268
|
+
?? (readStringValue(status.upstream) ? 'unchecked' : 'no_upstream'),
|
|
269
|
+
upstreamFetchedAt: readNumberValue(status.upstreamFetchedAt, status.upstream_fetched_at),
|
|
270
|
+
upstreamFetchError: readStringValue(status.upstreamFetchError, status.upstream_fetch_error),
|
|
210
271
|
ahead: readNumberValue(status.ahead) ?? 0,
|
|
211
272
|
behind: readNumberValue(status.behind) ?? 0,
|
|
212
273
|
staged: readNumberValue(status.staged) ?? 0,
|
|
@@ -217,14 +278,247 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
217
278
|
hasConflicts,
|
|
218
279
|
conflictFiles,
|
|
219
280
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
-
lastCheckedAt: Date.now(),
|
|
281
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
221
282
|
...(submodules ? { submodules } : {}),
|
|
222
283
|
};
|
|
223
284
|
}
|
|
224
285
|
|
|
286
|
+
function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
|
|
287
|
+
if (!git) return Number.NEGATIVE_INFINITY;
|
|
288
|
+
let score = 0;
|
|
289
|
+
if (readBooleanValue(git.isGitRepo) === true) score += 50;
|
|
290
|
+
if (readBooleanValue(git.isGitRepo) === false) score -= 10;
|
|
291
|
+
if (readStringValue(git.branch)) score += 20;
|
|
292
|
+
if (readStringValue(git.headCommit)) score += 20;
|
|
293
|
+
if (readStringValue(git.upstream)) score += 10;
|
|
294
|
+
if (readStringValue(git.upstreamStatus)) score += 5;
|
|
295
|
+
if (readNumberValue(git.ahead) !== undefined) score += 2;
|
|
296
|
+
if (readNumberValue(git.behind) !== undefined) score += 2;
|
|
297
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
|
|
298
|
+
if (readStringValue(git.error)) score -= 20;
|
|
299
|
+
return score;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
303
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
304
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
305
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
306
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
307
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
308
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
309
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
310
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
311
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
312
|
+
const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
|
|
313
|
+
let best: { git: Record<string, unknown>; score: number } | null = null;
|
|
314
|
+
for (const status of candidates) {
|
|
315
|
+
const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
316
|
+
if (!normalized) continue;
|
|
317
|
+
const score = scoreInlineMeshGitStatus(normalized);
|
|
318
|
+
if (!best || score > best.score) best = { git: normalized, score };
|
|
319
|
+
}
|
|
320
|
+
return best?.git;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
|
|
324
|
+
if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
|
|
325
|
+
return snapshot.nodes.some((node: any) => {
|
|
326
|
+
if (node?.gitProbePending !== true) return false;
|
|
327
|
+
const git = readObjectRecord(node?.git);
|
|
328
|
+
return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
333
|
+
const source = readStringValue(connection.source);
|
|
334
|
+
const transport = readStringValue(connection.transport);
|
|
335
|
+
return {
|
|
336
|
+
...connection,
|
|
337
|
+
perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
|
|
338
|
+
source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
|
|
339
|
+
state: 'connected',
|
|
340
|
+
transport: transport && transport !== 'unknown' ? transport : 'direct',
|
|
341
|
+
reported: true,
|
|
342
|
+
reason: 'Live peer git snapshot reported by the selected coordinator.',
|
|
343
|
+
lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function recordInlineMeshDirectGitTruth(
|
|
348
|
+
node: any,
|
|
349
|
+
git: Record<string, unknown>,
|
|
350
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
351
|
+
): void {
|
|
352
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
353
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
354
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
355
|
+
const nextGit: Record<string, unknown> = {
|
|
356
|
+
...git,
|
|
357
|
+
lastCheckedAt: checkedAt,
|
|
358
|
+
};
|
|
359
|
+
node.lastGit = {
|
|
360
|
+
source,
|
|
361
|
+
checkedAt,
|
|
362
|
+
status: nextGit,
|
|
363
|
+
};
|
|
364
|
+
node.last_git = node.lastGit;
|
|
365
|
+
node.machineStatus = 'online';
|
|
366
|
+
node.updatedAt = updatedAt;
|
|
367
|
+
node.lastSeenAt = updatedAt;
|
|
368
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
369
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
373
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
374
|
+
if (liveGit) return liveGit;
|
|
375
|
+
|
|
376
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
377
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
378
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
379
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
383
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
384
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
385
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
386
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
387
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
388
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
389
|
+
const branch = readStringValue(cachedGit.branch);
|
|
390
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
391
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
395
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
396
|
+
const {
|
|
397
|
+
cachedStatus,
|
|
398
|
+
lastGit: _lastGit,
|
|
399
|
+
last_git: _lastGitLegacy,
|
|
400
|
+
lastProbe: _lastProbe,
|
|
401
|
+
last_probe: _lastProbeLegacy,
|
|
402
|
+
error: _error,
|
|
403
|
+
health: _health,
|
|
404
|
+
machineStatus: _machineStatus,
|
|
405
|
+
lastSeenAt: _lastSeenAt,
|
|
406
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
407
|
+
updatedAt: _updatedAt,
|
|
408
|
+
updated_at: _updatedAtLegacy,
|
|
409
|
+
activeSession: _activeSession,
|
|
410
|
+
active_session: _activeSessionLegacy,
|
|
411
|
+
activeSessionId: _activeSessionId,
|
|
412
|
+
active_session_id: _activeSessionIdLegacy,
|
|
413
|
+
sessionId: _sessionId,
|
|
414
|
+
session_id: _sessionIdLegacy,
|
|
415
|
+
providerType: _providerType,
|
|
416
|
+
provider_type: _providerTypeLegacy,
|
|
417
|
+
...rest
|
|
418
|
+
} = node as Record<string, unknown>;
|
|
419
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
420
|
+
return { ...rest, cachedStatus };
|
|
421
|
+
}
|
|
422
|
+
return rest;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
426
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
427
|
+
return 'cachedStatus' in node
|
|
428
|
+
|| 'lastGit' in node
|
|
429
|
+
|| 'last_git' in node
|
|
430
|
+
|| 'lastProbe' in node
|
|
431
|
+
|| 'last_probe' in node
|
|
432
|
+
|| 'error' in node
|
|
433
|
+
|| 'health' in node
|
|
434
|
+
|| 'machineStatus' in node
|
|
435
|
+
|| 'lastSeenAt' in node
|
|
436
|
+
|| 'last_seen_at' in node
|
|
437
|
+
|| 'updatedAt' in node
|
|
438
|
+
|| 'updated_at' in node
|
|
439
|
+
|| 'activeSession' in node
|
|
440
|
+
|| 'active_session' in node
|
|
441
|
+
|| 'activeSessionId' in node
|
|
442
|
+
|| 'active_session_id' in node
|
|
443
|
+
|| 'sessionId' in node
|
|
444
|
+
|| 'session_id' in node
|
|
445
|
+
|| 'providerType' in node
|
|
446
|
+
|| 'provider_type' in node;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
450
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
451
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
452
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function readInlineMeshNodeId(node: any): string {
|
|
456
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
460
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
461
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
462
|
+
let changed = false;
|
|
463
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
464
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
465
|
+
changed = true;
|
|
466
|
+
return stripInlineMeshTransientNodeState(node);
|
|
467
|
+
});
|
|
468
|
+
if (!changed) return inlineMesh;
|
|
469
|
+
return {
|
|
470
|
+
...inlineMesh,
|
|
471
|
+
nodes,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
476
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
477
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
478
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
479
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
480
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
481
|
+
|
|
482
|
+
const cachedUpdatedAt = Date.parse(readStringValue(cached.updatedAt, cached.updated_at) || '');
|
|
483
|
+
const incomingUpdatedAt = Date.parse(readStringValue(incoming.updatedAt, incoming.updated_at) || '');
|
|
484
|
+
const preserveCachedMembership = Number.isFinite(cachedUpdatedAt)
|
|
485
|
+
&& (!Number.isFinite(incomingUpdatedAt) || cachedUpdatedAt > incomingUpdatedAt);
|
|
486
|
+
|
|
487
|
+
const cachedById = new Map<string, any>();
|
|
488
|
+
for (const node of cachedNodes) {
|
|
489
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
490
|
+
if (nodeId) cachedById.set(nodeId, node);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const nodes = incomingNodes.map((incomingNode: any) => {
|
|
494
|
+
const nodeId = readInlineMeshNodeId(incomingNode);
|
|
495
|
+
const cachedNode = nodeId ? cachedById.get(nodeId) : undefined;
|
|
496
|
+
if (!cachedNode && preserveCachedMembership) return null;
|
|
497
|
+
if (!cachedNode) return incomingNode;
|
|
498
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
499
|
+
return { ...cachedNode, ...incomingNode };
|
|
500
|
+
}
|
|
501
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
502
|
+
}).filter(Boolean);
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
...cached,
|
|
506
|
+
...incoming,
|
|
507
|
+
nodes,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
225
511
|
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
226
|
-
|
|
227
|
-
|
|
512
|
+
return countGitWorktreeChanges(git) > 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function countGitWorktreeChanges(git: Record<string, unknown> | null | undefined): number {
|
|
516
|
+
if (!git) return 0;
|
|
517
|
+
return Number(git.staged || 0)
|
|
518
|
+
+ Number(git.modified || 0)
|
|
519
|
+
+ Number(git.untracked || 0)
|
|
520
|
+
+ Number(git.deleted || 0)
|
|
521
|
+
+ Number(git.renamed || 0);
|
|
228
522
|
}
|
|
229
523
|
|
|
230
524
|
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
@@ -249,6 +543,167 @@ function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undef
|
|
|
249
543
|
return 'online';
|
|
250
544
|
}
|
|
251
545
|
|
|
546
|
+
function readMeshNodeLabel(status: Record<string, unknown>, node: any): string {
|
|
547
|
+
return readStringValue(status.nodeId, node?.id, node?.nodeId) ?? 'unknown';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function buildInlineMeshBranchConvergence(args: {
|
|
551
|
+
mesh: any;
|
|
552
|
+
node: any;
|
|
553
|
+
status: Record<string, unknown>;
|
|
554
|
+
}): Record<string, unknown> {
|
|
555
|
+
const git = readObjectRecord(args.status.git);
|
|
556
|
+
const nodeLabel = readMeshNodeLabel(args.status, args.node);
|
|
557
|
+
const defaultBranch = readStringValue(args.mesh?.defaultBranch) ?? 'main';
|
|
558
|
+
const branch = readStringValue(git.branch, args.node?.worktreeBranch) ?? null;
|
|
559
|
+
const upstream = readStringValue(git.upstream) ?? null;
|
|
560
|
+
const upstreamStatus = readStringValue(git.upstreamStatus, git.upstream_status)
|
|
561
|
+
?? (upstream ? 'unchecked' : 'no_upstream');
|
|
562
|
+
const ahead = readNumberValue(git.ahead) ?? 0;
|
|
563
|
+
const behind = readNumberValue(git.behind) ?? 0;
|
|
564
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
565
|
+
const hasConflicts = readBooleanValue(git.hasConflicts)
|
|
566
|
+
?? (Array.isArray(git.conflictFiles) && git.conflictFiles.length > 0);
|
|
567
|
+
const base = {
|
|
568
|
+
defaultBranch,
|
|
569
|
+
branch,
|
|
570
|
+
upstream,
|
|
571
|
+
upstreamStatus,
|
|
572
|
+
ahead,
|
|
573
|
+
behind,
|
|
574
|
+
isWorktree: args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true,
|
|
575
|
+
isDefaultBranch: branch === defaultBranch,
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
if (readBooleanValue(git.isGitRepo) !== true) {
|
|
579
|
+
return {
|
|
580
|
+
...base,
|
|
581
|
+
status: 'blocked_review',
|
|
582
|
+
needsConvergence: true,
|
|
583
|
+
reason: 'git_status_unavailable',
|
|
584
|
+
nextStep: `Resolve git status for node '${nodeLabel}' before marking the task complete.`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!branch) {
|
|
589
|
+
return {
|
|
590
|
+
...base,
|
|
591
|
+
status: 'blocked_review',
|
|
592
|
+
needsConvergence: true,
|
|
593
|
+
reason: 'branch_unknown',
|
|
594
|
+
nextStep: `Inspect node '${nodeLabel}' git branch before deciding whether it is merged to ${defaultBranch}.`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (hasConflicts || uncommittedChanges > 0) {
|
|
599
|
+
return {
|
|
600
|
+
...base,
|
|
601
|
+
status: 'not_mergeable',
|
|
602
|
+
needsConvergence: true,
|
|
603
|
+
reason: hasConflicts ? 'conflicts_present' : 'dirty_workspace',
|
|
604
|
+
nextStep: `Commit, checkpoint, or resolve node '${nodeLabel}' before any main convergence step.`,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (branch === defaultBranch) {
|
|
609
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
610
|
+
return {
|
|
611
|
+
...base,
|
|
612
|
+
status: 'blocked_review',
|
|
613
|
+
needsConvergence: true,
|
|
614
|
+
reason: 'default_branch_upstream_unverified',
|
|
615
|
+
nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${nodeLabel}'.`,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (ahead > 0 || behind > 0) {
|
|
619
|
+
return {
|
|
620
|
+
...base,
|
|
621
|
+
status: 'blocked_review',
|
|
622
|
+
needsConvergence: true,
|
|
623
|
+
reason: 'default_branch_not_even_with_upstream',
|
|
624
|
+
nextStep: `Bring ${defaultBranch} even with its upstream before declaring convergence complete.`,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
return {
|
|
628
|
+
...base,
|
|
629
|
+
status: 'merged_to_main',
|
|
630
|
+
needsConvergence: false,
|
|
631
|
+
reason: 'clean_default_branch',
|
|
632
|
+
nextStep: null,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true) {
|
|
637
|
+
return {
|
|
638
|
+
...base,
|
|
639
|
+
status: 'cleanup_candidate',
|
|
640
|
+
needsConvergence: true,
|
|
641
|
+
reason: 'clean_non_default_worktree_branch',
|
|
642
|
+
nextStep: `Run mesh_refine_node(node_id: "${nodeLabel}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
647
|
+
return {
|
|
648
|
+
...base,
|
|
649
|
+
status: 'blocked_review',
|
|
650
|
+
needsConvergence: true,
|
|
651
|
+
reason: 'feature_branch_upstream_unverified',
|
|
652
|
+
nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!upstream || ahead > 0 || behind > 0) {
|
|
657
|
+
return {
|
|
658
|
+
...base,
|
|
659
|
+
status: 'blocked_review',
|
|
660
|
+
needsConvergence: true,
|
|
661
|
+
reason: !upstream ? 'feature_branch_missing_upstream' : 'feature_branch_not_even_with_upstream',
|
|
662
|
+
nextStep: `Push or reconcile branch '${branch}', then merge it into ${defaultBranch} or mark it not_mergeable with a reason.`,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
...base,
|
|
668
|
+
status: 'pushed_feature_branch_needs_merge',
|
|
669
|
+
needsConvergence: true,
|
|
670
|
+
reason: 'clean_non_default_branch',
|
|
671
|
+
nextStep: `Review and merge branch '${branch}' into ${defaultBranch}; do not report the task as fully complete while it remains off main.`,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
676
|
+
const git = readObjectRecord(status.git);
|
|
677
|
+
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
678
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
679
|
+
status.isDirty = uncommittedChanges > 0;
|
|
680
|
+
status.uncommittedChanges = uncommittedChanges;
|
|
681
|
+
status.branchConvergence = buildInlineMeshBranchConvergence({ mesh, node, status });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
685
|
+
const followUps = nodes
|
|
686
|
+
.filter(node => readObjectRecord(node.branchConvergence).needsConvergence === true)
|
|
687
|
+
.map(node => {
|
|
688
|
+
const convergence = readObjectRecord(node.branchConvergence);
|
|
689
|
+
return {
|
|
690
|
+
nodeId: node.nodeId,
|
|
691
|
+
workspace: node.workspace,
|
|
692
|
+
branch: convergence.branch,
|
|
693
|
+
status: convergence.status,
|
|
694
|
+
reason: convergence.reason,
|
|
695
|
+
nextStep: convergence.nextStep,
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
needsFollowUp: followUps.length > 0,
|
|
701
|
+
unresolvedCount: followUps.length,
|
|
702
|
+
requiredFinalStates: ['merged_to_main', 'pushed_feature_branch_needs_merge', 'blocked_review', 'cleanup_candidate', 'not_mergeable'],
|
|
703
|
+
followUps,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
252
707
|
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
253
708
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
254
709
|
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
@@ -313,6 +768,152 @@ function toIsoTimestamp(value: unknown): string | null {
|
|
|
313
768
|
return stringValue || null;
|
|
314
769
|
}
|
|
315
770
|
|
|
771
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
772
|
+
const connection = readObjectRecord(status.connection);
|
|
773
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
774
|
+
const git = readObjectRecord(status.git);
|
|
775
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
776
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
777
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
778
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function finalizeMeshNodeStatus(args: {
|
|
783
|
+
status: Record<string, unknown>;
|
|
784
|
+
node: any;
|
|
785
|
+
daemonId?: string;
|
|
786
|
+
isSelfNode: boolean;
|
|
787
|
+
}): void {
|
|
788
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
789
|
+
if (!readStringValue(status.machineStatus)) {
|
|
790
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
791
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
792
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
793
|
+
}
|
|
794
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
795
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
796
|
+
status.launchReady = !!daemonId && (
|
|
797
|
+
readStringValue(status.machineStatus) === 'online'
|
|
798
|
+
|| connectionState === 'connected'
|
|
799
|
+
|| isSelfNode
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
804
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
805
|
+
daemonId: string;
|
|
806
|
+
workspace: string;
|
|
807
|
+
timeoutMs: number;
|
|
808
|
+
}): Promise<Record<string, unknown> | null> {
|
|
809
|
+
if (!args.dispatchMeshCommand) return null;
|
|
810
|
+
const remoteResult = await Promise.race([
|
|
811
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
|
|
812
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
813
|
+
]) as any;
|
|
814
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
815
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
816
|
+
? remoteGit as Record<string, unknown>
|
|
817
|
+
: null;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
821
|
+
mesh: any;
|
|
822
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
823
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
824
|
+
statusInstanceId?: string;
|
|
825
|
+
localMachineId?: string;
|
|
826
|
+
}): Promise<{
|
|
827
|
+
directEvidenceCount: number;
|
|
828
|
+
localConfirmedCount: number;
|
|
829
|
+
peerAttemptedCount: number;
|
|
830
|
+
peerConfirmedCount: number;
|
|
831
|
+
unavailableNodeIds: string[];
|
|
832
|
+
}> {
|
|
833
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
834
|
+
if (!nodes.length) {
|
|
835
|
+
return {
|
|
836
|
+
directEvidenceCount: 0,
|
|
837
|
+
localConfirmedCount: 0,
|
|
838
|
+
peerAttemptedCount: 0,
|
|
839
|
+
peerConfirmedCount: 0,
|
|
840
|
+
unavailableNodeIds: [],
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
845
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
846
|
+
nodes[0]?.id,
|
|
847
|
+
nodes[0]?.nodeId,
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
let localConfirmedCount = 0;
|
|
851
|
+
let peerAttemptedCount = 0;
|
|
852
|
+
let peerConfirmedCount = 0;
|
|
853
|
+
const unavailableNodeIds: string[] = [];
|
|
854
|
+
|
|
855
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
856
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
857
|
+
const workspace = readStringValue(node?.workspace);
|
|
858
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
859
|
+
const isSelfNode = Boolean(
|
|
860
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
861
|
+
) || Boolean(
|
|
862
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
863
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
864
|
+
|
|
865
|
+
if (!workspace) {
|
|
866
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (fs.existsSync(workspace)) {
|
|
871
|
+
try {
|
|
872
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
873
|
+
if (localGit?.isGitRepo) {
|
|
874
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
875
|
+
localConfirmedCount += 1;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
} catch {
|
|
879
|
+
// Fall through to remote classification.
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
884
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
peerAttemptedCount += 1;
|
|
889
|
+
try {
|
|
890
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
891
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
892
|
+
daemonId,
|
|
893
|
+
workspace,
|
|
894
|
+
timeoutMs: 8_000,
|
|
895
|
+
});
|
|
896
|
+
if (remoteGit) {
|
|
897
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
898
|
+
peerConfirmedCount += 1;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
} catch {
|
|
902
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
unavailableNodeIds.push(nodeId);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return {
|
|
909
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
910
|
+
localConfirmedCount,
|
|
911
|
+
peerAttemptedCount,
|
|
912
|
+
peerConfirmedCount,
|
|
913
|
+
unavailableNodeIds,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
316
917
|
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
317
918
|
return {
|
|
318
919
|
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
@@ -328,11 +929,85 @@ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
|
328
929
|
};
|
|
329
930
|
}
|
|
330
931
|
|
|
331
|
-
function
|
|
932
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
933
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
934
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
935
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
936
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
940
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
941
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
942
|
+
|
|
943
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
944
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
945
|
+
|
|
946
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
950
|
+
meshId: string;
|
|
951
|
+
nodeId: string;
|
|
952
|
+
liveSessionRecords: any[];
|
|
953
|
+
allowCoordinatorSession?: boolean;
|
|
954
|
+
}): string {
|
|
955
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
956
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
957
|
+
&& readStringValue(record?.workspace)
|
|
958
|
+
));
|
|
959
|
+
if (directNodeWorkspace) {
|
|
960
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (args.allowCoordinatorSession) {
|
|
964
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
965
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
966
|
+
&& readStringValue(record?.workspace)
|
|
967
|
+
));
|
|
968
|
+
if (coordinatorWorkspace) {
|
|
969
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return '';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function collectLiveMeshSessionRecords(args: {
|
|
977
|
+
meshId: string;
|
|
978
|
+
node: any;
|
|
979
|
+
nodeId: string;
|
|
980
|
+
liveSessionRecords: any[];
|
|
981
|
+
allowCoordinatorSession?: boolean;
|
|
982
|
+
}): any[] {
|
|
983
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
984
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
985
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
|
|
986
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
if (args.allowCoordinatorSession) {
|
|
990
|
+
for (const record of args.liveSessionRecords) {
|
|
991
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
992
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
993
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
994
|
+
matches.push(record);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return matches;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function applyCachedInlineMeshNodeStatus(
|
|
1002
|
+
status: Record<string, unknown>,
|
|
1003
|
+
node: any,
|
|
1004
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
1005
|
+
): boolean {
|
|
332
1006
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const
|
|
1007
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
1008
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
1009
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
1010
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
336
1011
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
337
1012
|
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
338
1013
|
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
@@ -389,13 +1064,7 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
389
1064
|
}
|
|
390
1065
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
391
1066
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
392
|
-
type MeshRefineValidationCommand =
|
|
393
|
-
command: string;
|
|
394
|
-
args: string[];
|
|
395
|
-
displayCommand: string;
|
|
396
|
-
category: string;
|
|
397
|
-
source: string;
|
|
398
|
-
};
|
|
1067
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
399
1068
|
|
|
400
1069
|
type MeshRefineValidationSummary = {
|
|
401
1070
|
status: MeshRefineValidationStatus;
|
|
@@ -405,13 +1074,90 @@ type MeshRefineValidationSummary = {
|
|
|
405
1074
|
skippedReason?: string;
|
|
406
1075
|
timeoutMs: number;
|
|
407
1076
|
outputLimitBytes: number;
|
|
1077
|
+
configSource?: string;
|
|
1078
|
+
configSourceType?: string;
|
|
1079
|
+
suggestions?: unknown[];
|
|
1080
|
+
suggestedConfig?: unknown;
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
1084
|
+
|
|
1085
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
1086
|
+
status: MeshRefineStageStatus;
|
|
1087
|
+
equivalent: boolean;
|
|
1088
|
+
baseHead: string;
|
|
1089
|
+
branchHead: string;
|
|
1090
|
+
mergeBase?: string;
|
|
1091
|
+
mergedTree?: string;
|
|
1092
|
+
expectedPatchId?: string;
|
|
1093
|
+
actualPatchId?: string;
|
|
1094
|
+
durationMs: number;
|
|
1095
|
+
error?: string;
|
|
1096
|
+
stdout?: string;
|
|
1097
|
+
stderr?: string;
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
type MeshRefineSubmoduleReachabilityEntry = {
|
|
1101
|
+
path: string;
|
|
1102
|
+
commit: string;
|
|
1103
|
+
reachable: boolean;
|
|
1104
|
+
publishRequired?: boolean;
|
|
1105
|
+
checkedLocal?: boolean;
|
|
1106
|
+
localReachable?: boolean;
|
|
1107
|
+
remote?: string;
|
|
1108
|
+
remoteUrl?: string;
|
|
1109
|
+
remoteReachable?: boolean;
|
|
1110
|
+
remoteMainBranch?: string;
|
|
1111
|
+
remoteMainReachable?: boolean;
|
|
1112
|
+
fetchedFromOrigin?: boolean;
|
|
1113
|
+
error?: string;
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
type MeshRefineSubmoduleReachabilitySummary = {
|
|
1117
|
+
status: MeshRefineStageStatus;
|
|
1118
|
+
checked: number;
|
|
1119
|
+
unreachable: MeshRefineSubmoduleReachabilityEntry[];
|
|
1120
|
+
entries: MeshRefineSubmoduleReachabilityEntry[];
|
|
1121
|
+
durationMs: number;
|
|
1122
|
+
error?: string;
|
|
408
1123
|
};
|
|
409
1124
|
|
|
1125
|
+
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1126
|
+
|
|
1127
|
+
type MeshRefineJobHandle = {
|
|
1128
|
+
success: true;
|
|
1129
|
+
async: true;
|
|
1130
|
+
status: MeshRefineAsyncJobStatus;
|
|
1131
|
+
jobId: string;
|
|
1132
|
+
interactionId: string;
|
|
1133
|
+
meshId: string;
|
|
1134
|
+
nodeId: string;
|
|
1135
|
+
targetNodeId: string;
|
|
1136
|
+
targetDaemonId?: string;
|
|
1137
|
+
workspace?: string;
|
|
1138
|
+
startedAt: string;
|
|
1139
|
+
completedAt?: string;
|
|
1140
|
+
duplicate?: boolean;
|
|
1141
|
+
retryOfJobId?: string;
|
|
1142
|
+
eventDelivery: {
|
|
1143
|
+
pendingEvents: true;
|
|
1144
|
+
ledger: true;
|
|
1145
|
+
};
|
|
1146
|
+
evidence: {
|
|
1147
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
1148
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
1149
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
1150
|
+
};
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
1154
|
+
|
|
410
1155
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
411
1156
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
412
1157
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
413
1158
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
414
1159
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
1160
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
415
1161
|
|
|
416
1162
|
function truncateValidationOutput(value: unknown): string {
|
|
417
1163
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -419,171 +1165,234 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
419
1165
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
420
1166
|
}
|
|
421
1167
|
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
1168
|
+
function recordMeshRefineStage(
|
|
1169
|
+
stages: Array<Record<string, unknown>>,
|
|
1170
|
+
stage: string,
|
|
1171
|
+
status: MeshRefineStageStatus,
|
|
1172
|
+
startedAt: number,
|
|
1173
|
+
details?: Record<string, unknown>,
|
|
1174
|
+
): void {
|
|
1175
|
+
stages.push({
|
|
1176
|
+
stage,
|
|
1177
|
+
status,
|
|
1178
|
+
durationMs: Date.now() - startedAt,
|
|
1179
|
+
...(details || {}),
|
|
1180
|
+
});
|
|
432
1181
|
}
|
|
433
1182
|
|
|
434
|
-
function
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
440
|
-
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
441
|
-
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
442
|
-
if (!tokens.length) return null;
|
|
443
|
-
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
444
|
-
return tokens;
|
|
1183
|
+
function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
|
|
1184
|
+
const refs = entries
|
|
1185
|
+
.map(entry => `${entry.path}@${entry.commit}`)
|
|
1186
|
+
.join(', ');
|
|
1187
|
+
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.`;
|
|
445
1188
|
}
|
|
446
1189
|
|
|
447
|
-
function
|
|
448
|
-
|
|
1190
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
1191
|
+
const { execFileSync } = await import('node:child_process');
|
|
1192
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
1193
|
+
cwd,
|
|
1194
|
+
encoding: 'utf8',
|
|
1195
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1196
|
+
});
|
|
1197
|
+
if (!diff.trim()) return '';
|
|
1198
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
1199
|
+
cwd,
|
|
1200
|
+
input: diff,
|
|
1201
|
+
encoding: 'utf8',
|
|
1202
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1203
|
+
}).trim();
|
|
1204
|
+
return patchId.split(/\s+/)[0] || '';
|
|
449
1205
|
}
|
|
450
1206
|
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1207
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
1208
|
+
repoRoot: string,
|
|
1209
|
+
baseHead: string,
|
|
1210
|
+
branchHead: string,
|
|
1211
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
1212
|
+
const startedAt = Date.now();
|
|
1213
|
+
try {
|
|
1214
|
+
const { execFileSync } = await import('node:child_process');
|
|
1215
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
1216
|
+
cwd: repoRoot,
|
|
1217
|
+
encoding: 'utf8',
|
|
1218
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1219
|
+
});
|
|
1220
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
1221
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
1222
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
1223
|
+
if (!mergeBase || !mergedTree) {
|
|
1224
|
+
return {
|
|
1225
|
+
status: 'failed',
|
|
1226
|
+
equivalent: false,
|
|
1227
|
+
baseHead,
|
|
1228
|
+
branchHead,
|
|
1229
|
+
mergeBase: mergeBase || undefined,
|
|
1230
|
+
mergedTree: mergedTree || undefined,
|
|
1231
|
+
durationMs: Date.now() - startedAt,
|
|
1232
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
1233
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
1237
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
1238
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
1239
|
+
return {
|
|
1240
|
+
status: equivalent ? 'passed' : 'failed',
|
|
1241
|
+
equivalent,
|
|
1242
|
+
baseHead,
|
|
1243
|
+
branchHead,
|
|
1244
|
+
mergeBase,
|
|
1245
|
+
mergedTree,
|
|
1246
|
+
expectedPatchId,
|
|
1247
|
+
actualPatchId,
|
|
1248
|
+
durationMs: Date.now() - startedAt,
|
|
1249
|
+
};
|
|
1250
|
+
} catch (e: any) {
|
|
1251
|
+
return {
|
|
1252
|
+
status: 'failed',
|
|
1253
|
+
equivalent: false,
|
|
1254
|
+
baseHead,
|
|
1255
|
+
branchHead,
|
|
1256
|
+
durationMs: Date.now() - startedAt,
|
|
1257
|
+
error: e?.message || String(e),
|
|
1258
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1259
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1260
|
+
};
|
|
488
1261
|
}
|
|
489
|
-
|
|
490
|
-
return {
|
|
491
|
-
command: {
|
|
492
|
-
command,
|
|
493
|
-
args,
|
|
494
|
-
displayCommand: [command, ...args].join(' '),
|
|
495
|
-
category,
|
|
496
|
-
source,
|
|
497
|
-
},
|
|
498
|
-
};
|
|
499
1262
|
}
|
|
500
1263
|
|
|
501
|
-
function
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1264
|
+
async function runMeshRefineSubmoduleReachabilityGate(
|
|
1265
|
+
repoRoot: string,
|
|
1266
|
+
mergedTree: string,
|
|
1267
|
+
): Promise<MeshRefineSubmoduleReachabilitySummary> {
|
|
1268
|
+
const startedAt = Date.now();
|
|
1269
|
+
const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
|
|
1270
|
+
try {
|
|
1271
|
+
const { execFile } = await import('node:child_process');
|
|
1272
|
+
const { promisify } = await import('node:util');
|
|
1273
|
+
const execFileAsync = promisify(execFile);
|
|
1274
|
+
const runGit = async (cwd: string, args: string[]): Promise<string> => {
|
|
1275
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
1276
|
+
cwd,
|
|
1277
|
+
encoding: 'utf8',
|
|
1278
|
+
timeout: 30_000,
|
|
1279
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1280
|
+
windowsHide: true,
|
|
514
1281
|
});
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
1282
|
+
return String(stdout || '');
|
|
1283
|
+
};
|
|
1284
|
+
const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
|
|
1285
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1286
|
+
await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
|
|
1287
|
+
};
|
|
522
1288
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
1289
|
+
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1290
|
+
const gitlinks = treeOutput
|
|
1291
|
+
.split('\0')
|
|
1292
|
+
.filter(Boolean)
|
|
1293
|
+
.map(record => {
|
|
1294
|
+
const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
1295
|
+
return match ? { commit: match[1], path: match[2] } : null;
|
|
1296
|
+
})
|
|
1297
|
+
.filter((entry): entry is { commit: string; path: string } => !!entry);
|
|
1298
|
+
|
|
1299
|
+
for (const gitlink of gitlinks) {
|
|
1300
|
+
const submodulePath = pathResolve(repoRoot, gitlink.path);
|
|
1301
|
+
const entry: MeshRefineSubmoduleReachabilityEntry = {
|
|
1302
|
+
path: gitlink.path,
|
|
1303
|
+
commit: gitlink.commit,
|
|
1304
|
+
reachable: false,
|
|
1305
|
+
};
|
|
1306
|
+
try {
|
|
1307
|
+
if (!fs.existsSync(submodulePath)) {
|
|
1308
|
+
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1309
|
+
entry.publishRequired = true;
|
|
1310
|
+
entries.push(entry);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
540
1313
|
|
|
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
|
-
|
|
1314
|
+
entry.checkedLocal = true;
|
|
1315
|
+
try {
|
|
1316
|
+
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1317
|
+
entry.localReachable = true;
|
|
1318
|
+
} catch {
|
|
1319
|
+
entry.localReachable = false;
|
|
1320
|
+
// Probe the submodule remote before allowing cleanup/completion.
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
try {
|
|
1324
|
+
entry.remote = 'origin';
|
|
1325
|
+
let remoteUrl = '';
|
|
1326
|
+
try {
|
|
1327
|
+
remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
|
|
1328
|
+
if (!remoteUrl) throw new Error('origin remote has no URL');
|
|
1329
|
+
entry.remoteUrl = remoteUrl;
|
|
1330
|
+
} catch {
|
|
1331
|
+
entry.error = 'Submodule remote reachability check failed: no configured origin remote';
|
|
1332
|
+
entry.publishRequired = true;
|
|
1333
|
+
entries.push(entry);
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
entry.remoteMainBranch = 'main';
|
|
1337
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1338
|
+
entry.fetchedFromOrigin = true;
|
|
1339
|
+
entry.remoteReachable = true;
|
|
1340
|
+
entry.remoteMainReachable = true;
|
|
1341
|
+
entry.reachable = true;
|
|
1342
|
+
} catch (e: any) {
|
|
1343
|
+
entry.remoteReachable = false;
|
|
1344
|
+
entry.remoteMainReachable = false;
|
|
1345
|
+
entry.publishRequired = true;
|
|
1346
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1347
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1348
|
+
}
|
|
1349
|
+
} catch (e: any) {
|
|
1350
|
+
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
1351
|
+
entry.publishRequired = true;
|
|
1352
|
+
}
|
|
1353
|
+
entries.push(entry);
|
|
574
1354
|
}
|
|
1355
|
+
|
|
1356
|
+
const unreachable = entries.filter(entry => !entry.reachable);
|
|
1357
|
+
return {
|
|
1358
|
+
status: unreachable.length ? 'failed' : 'passed',
|
|
1359
|
+
checked: entries.length,
|
|
1360
|
+
unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
|
|
1361
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
|
|
1362
|
+
durationMs: Date.now() - startedAt,
|
|
1363
|
+
};
|
|
1364
|
+
} catch (e: any) {
|
|
1365
|
+
const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
|
|
1366
|
+
return {
|
|
1367
|
+
status: 'failed',
|
|
1368
|
+
checked: entries.length,
|
|
1369
|
+
unreachable,
|
|
1370
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
|
|
1371
|
+
durationMs: Date.now() - startedAt,
|
|
1372
|
+
error: truncateValidationOutput(e?.message || String(e)),
|
|
1373
|
+
};
|
|
575
1374
|
}
|
|
1375
|
+
}
|
|
576
1376
|
|
|
1377
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1378
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
577
1379
|
return {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
:
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1380
|
+
source: plan.source,
|
|
1381
|
+
sourceType: plan.sourceType,
|
|
1382
|
+
commands: plan.commands.map(command => ({
|
|
1383
|
+
displayCommand: command.displayCommand,
|
|
1384
|
+
category: command.category,
|
|
1385
|
+
source: command.source,
|
|
1386
|
+
cwd: command.cwd,
|
|
1387
|
+
timeoutMs: command.timeoutMs,
|
|
1388
|
+
})),
|
|
1389
|
+
unavailableReason: plan.unavailableReason,
|
|
1390
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1391
|
+
suggestions: plan.suggestions,
|
|
1392
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1393
|
+
note: plan.sourceType === 'unavailable'
|
|
1394
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1395
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
587
1396
|
};
|
|
588
1397
|
}
|
|
589
1398
|
|
|
@@ -591,7 +1400,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
591
1400
|
const { execFile } = await import('node:child_process');
|
|
592
1401
|
const { promisify } = await import('node:util');
|
|
593
1402
|
const execFileAsync = promisify(execFile);
|
|
594
|
-
const selection =
|
|
1403
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
595
1404
|
const summary: MeshRefineValidationSummary = {
|
|
596
1405
|
status: 'skipped',
|
|
597
1406
|
required: true,
|
|
@@ -600,22 +1409,28 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
600
1409
|
skippedReason: undefined,
|
|
601
1410
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
602
1411
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1412
|
+
configSource: selection.source,
|
|
1413
|
+
configSourceType: selection.sourceType,
|
|
1414
|
+
suggestions: selection.suggestions,
|
|
1415
|
+
suggestedConfig: selection.suggestedConfig,
|
|
603
1416
|
};
|
|
604
1417
|
|
|
605
1418
|
if (!selection.commands.length) {
|
|
606
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1419
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
607
1420
|
return summary;
|
|
608
1421
|
}
|
|
609
1422
|
|
|
610
1423
|
for (const candidate of selection.commands) {
|
|
611
1424
|
const startedAt = Date.now();
|
|
1425
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1426
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
612
1427
|
try {
|
|
613
1428
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
614
|
-
cwd
|
|
1429
|
+
cwd,
|
|
615
1430
|
encoding: 'utf8',
|
|
616
|
-
timeout
|
|
1431
|
+
timeout,
|
|
617
1432
|
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
618
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
1433
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
619
1434
|
});
|
|
620
1435
|
summary.commandsRun.push({
|
|
621
1436
|
command: candidate.command,
|
|
@@ -623,6 +1438,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
623
1438
|
displayCommand: candidate.displayCommand,
|
|
624
1439
|
category: candidate.category,
|
|
625
1440
|
source: candidate.source,
|
|
1441
|
+
cwd,
|
|
626
1442
|
passed: true,
|
|
627
1443
|
exitCode: 0,
|
|
628
1444
|
durationMs: Date.now() - startedAt,
|
|
@@ -636,6 +1452,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
636
1452
|
displayCommand: candidate.displayCommand,
|
|
637
1453
|
category: candidate.category,
|
|
638
1454
|
source: candidate.source,
|
|
1455
|
+
cwd,
|
|
639
1456
|
passed: false,
|
|
640
1457
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
641
1458
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
@@ -776,6 +1593,8 @@ export interface CommandRouterDeps {
|
|
|
776
1593
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
777
1594
|
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
778
1595
|
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1596
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
1597
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
779
1598
|
}
|
|
780
1599
|
|
|
781
1600
|
export interface CommandRouterResult {
|
|
@@ -887,42 +1706,266 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
887
1706
|
};
|
|
888
1707
|
}
|
|
889
1708
|
|
|
1709
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
1710
|
+
const raw = hostAddress.trim();
|
|
1711
|
+
if (!raw) throw new Error('hostAddress required');
|
|
1712
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
1713
|
+
url.pathname = '/api/v1/command';
|
|
1714
|
+
url.search = '';
|
|
1715
|
+
url.hash = '';
|
|
1716
|
+
return url.toString();
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
1720
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
1721
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
1722
|
+
? args.memberNode as Record<string, any>
|
|
1723
|
+
: null;
|
|
1724
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
1725
|
+
? (requestedNodeId
|
|
1726
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
1727
|
+
: mesh.nodes[0])
|
|
1728
|
+
: null;
|
|
1729
|
+
const source = explicit || configured;
|
|
1730
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
1731
|
+
? source.workspace.trim()
|
|
1732
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
1733
|
+
? args.workspace.trim()
|
|
1734
|
+
: process.cwd();
|
|
1735
|
+
if (!workspace) return null;
|
|
1736
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
1737
|
+
? source.id.trim()
|
|
1738
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
1739
|
+
? source.nodeId.trim()
|
|
1740
|
+
: undefined;
|
|
1741
|
+
return {
|
|
1742
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
1743
|
+
workspace,
|
|
1744
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
1745
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
1746
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
1747
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
1748
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
1749
|
+
role: 'member',
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
890
1753
|
export class DaemonCommandRouter {
|
|
891
1754
|
private deps: CommandRouterDeps;
|
|
892
1755
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
893
1756
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
894
1757
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
895
1758
|
private inlineMeshCache = new Map<string, any>();
|
|
1759
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
1760
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
|
|
1761
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
1762
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
1763
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
1764
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
896
1765
|
|
|
897
1766
|
constructor(deps: CommandRouterDeps) {
|
|
898
1767
|
this.deps = deps;
|
|
899
1768
|
}
|
|
900
1769
|
|
|
1770
|
+
private cloneJsonValue<T>(value: T): T {
|
|
1771
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
1772
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
1776
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
1777
|
+
const inlineNodesById = new Map<string, any>();
|
|
1778
|
+
for (const node of mesh.nodes) {
|
|
1779
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
1780
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
1781
|
+
}
|
|
1782
|
+
if (!inlineNodesById.size) return snapshot;
|
|
1783
|
+
|
|
1784
|
+
let changed = false;
|
|
1785
|
+
const unavailableNodeIds = new Set<string>();
|
|
1786
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
1787
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
1788
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
1789
|
+
const nodeId = readStringValue(entry);
|
|
1790
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
1794
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
1795
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
1796
|
+
if (!inlineNode) return statusNode;
|
|
1797
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
1798
|
+
if (!liveGit) return statusNode;
|
|
1799
|
+
const nextStatus = { ...statusNode };
|
|
1800
|
+
nextStatus.git = liveGit;
|
|
1801
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
1802
|
+
applyInlineMeshBranchConvergence(mesh, inlineNode, nextStatus);
|
|
1803
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
1804
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
1805
|
+
const connectionState = readStringValue(connection.state);
|
|
1806
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
1807
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
1808
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
1809
|
+
}
|
|
1810
|
+
delete nextStatus.gitProbePending;
|
|
1811
|
+
const error = readStringValue(nextStatus.error);
|
|
1812
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
1813
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
1814
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
1815
|
+
changed = true;
|
|
1816
|
+
return nextStatus;
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
const aggregateDirectTruthSatisfied = sourceOfTruth.coordinatorOwnsLiveTruth === true
|
|
1820
|
+
|| directPeerTruth.satisfied === true;
|
|
1821
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied)) return snapshot;
|
|
1822
|
+
const nextSourceOfTruth = {
|
|
1823
|
+
...sourceOfTruth,
|
|
1824
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
1825
|
+
directPeerTruth: {
|
|
1826
|
+
...directPeerTruth,
|
|
1827
|
+
satisfied: options?.requireDirectPeerTruth === true
|
|
1828
|
+
? aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0
|
|
1829
|
+
: directPeerTruth.satisfied,
|
|
1830
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
1831
|
+
},
|
|
1832
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
1833
|
+
coordinatorOwnsLiveTruth: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0,
|
|
1834
|
+
currentStatus: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
1835
|
+
} : {}),
|
|
1836
|
+
} : {}),
|
|
1837
|
+
};
|
|
1838
|
+
return {
|
|
1839
|
+
...snapshot,
|
|
1840
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied ? {
|
|
1841
|
+
success: false,
|
|
1842
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
1843
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
1844
|
+
} : {}),
|
|
1845
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
1846
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodes),
|
|
1847
|
+
nodes,
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
1852
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
1853
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
1854
|
+
if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
|
|
1855
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
1856
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
1857
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
1858
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
1859
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
1860
|
+
? snapshot.sourceOfTruth
|
|
1861
|
+
: {};
|
|
1862
|
+
snapshot.sourceOfTruth = {
|
|
1863
|
+
...sourceOfTruth,
|
|
1864
|
+
aggregateSnapshot: {
|
|
1865
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
1866
|
+
? sourceOfTruth.aggregateSnapshot
|
|
1867
|
+
: {}),
|
|
1868
|
+
owner: 'coordinator_daemon_memory',
|
|
1869
|
+
cached: true,
|
|
1870
|
+
source: 'memory',
|
|
1871
|
+
refreshReason: 'memory_cache_hit',
|
|
1872
|
+
ageMs,
|
|
1873
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
1874
|
+
returnedAt: new Date().toISOString(),
|
|
1875
|
+
},
|
|
1876
|
+
};
|
|
1877
|
+
return snapshot;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
1881
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
1882
|
+
const builtAt = Date.now();
|
|
1883
|
+
const next = this.cloneJsonValue(snapshot);
|
|
1884
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
1885
|
+
? next.sourceOfTruth
|
|
1886
|
+
: {};
|
|
1887
|
+
next.sourceOfTruth = {
|
|
1888
|
+
...sourceOfTruth,
|
|
1889
|
+
aggregateSnapshot: {
|
|
1890
|
+
owner: 'coordinator_daemon_memory',
|
|
1891
|
+
cached: false,
|
|
1892
|
+
source: 'live_refresh',
|
|
1893
|
+
refreshReason,
|
|
1894
|
+
ageMs: 0,
|
|
1895
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
1896
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
1897
|
+
},
|
|
1898
|
+
};
|
|
1899
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
|
|
1900
|
+
return next;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
901
1903
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
902
1904
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
903
|
-
this.
|
|
904
|
-
return inlineMesh as any;
|
|
1905
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
905
1906
|
}
|
|
906
1907
|
return this.inlineMeshCache.get(meshId);
|
|
907
1908
|
}
|
|
908
1909
|
|
|
1910
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
1911
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
1912
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
1913
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
1914
|
+
if (cached) {
|
|
1915
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
1916
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
1917
|
+
return merged;
|
|
1918
|
+
}
|
|
1919
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
1920
|
+
return sanitizedInlineMesh as any;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
909
1923
|
private async getMeshForCommand(
|
|
910
1924
|
meshId: string,
|
|
911
1925
|
inlineMesh?: unknown,
|
|
912
1926
|
options?: { preferInline?: boolean },
|
|
913
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
1927
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
914
1928
|
const preferInline = options?.preferInline === true;
|
|
915
1929
|
if (preferInline) {
|
|
916
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
917
|
-
if (cached)
|
|
1930
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1931
|
+
if (cached) {
|
|
1932
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1933
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
1934
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
1935
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
1936
|
+
}
|
|
1937
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1938
|
+
}
|
|
1939
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1940
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1941
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
1942
|
+
}
|
|
918
1943
|
}
|
|
919
1944
|
try {
|
|
920
1945
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
921
1946
|
const mesh = getMesh(meshId);
|
|
922
|
-
if (mesh) return { mesh, inline: false };
|
|
1947
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
923
1948
|
} catch { /* fall through to inline cache */ }
|
|
924
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
925
|
-
|
|
1949
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1950
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1951
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1952
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
1956
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
1961
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
1962
|
+
const mesh = meshRecord?.mesh;
|
|
1963
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1964
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
1965
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
1966
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
1967
|
+
}
|
|
1968
|
+
return null;
|
|
926
1969
|
}
|
|
927
1970
|
|
|
928
1971
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -932,6 +1975,7 @@ export class DaemonCommandRouter {
|
|
|
932
1975
|
else mesh.nodes.push(node);
|
|
933
1976
|
mesh.updatedAt = new Date().toISOString();
|
|
934
1977
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1978
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
935
1979
|
}
|
|
936
1980
|
|
|
937
1981
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -941,6 +1985,7 @@ export class DaemonCommandRouter {
|
|
|
941
1985
|
mesh.nodes.splice(idx, 1);
|
|
942
1986
|
mesh.updatedAt = new Date().toISOString();
|
|
943
1987
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1988
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
944
1989
|
return true;
|
|
945
1990
|
}
|
|
946
1991
|
|
|
@@ -1219,6 +2264,7 @@ export class DaemonCommandRouter {
|
|
|
1219
2264
|
const deletedSessionIds: string[] = [];
|
|
1220
2265
|
const skippedSessionIds: string[] = [];
|
|
1221
2266
|
const skippedLiveSessionIds: string[] = [];
|
|
2267
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1222
2268
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1223
2269
|
const recordsRemainSessionIds: string[] = [];
|
|
1224
2270
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1253,6 +2299,12 @@ export class DaemonCommandRouter {
|
|
|
1253
2299
|
const completed = this.isCompletedHostedSession(record);
|
|
1254
2300
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1255
2301
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
2302
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
2303
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
2304
|
+
skippedSessionIds.push(sessionId);
|
|
2305
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
1256
2308
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1257
2309
|
skippedSessionIds.push(sessionId);
|
|
1258
2310
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1322,6 +2374,7 @@ export class DaemonCommandRouter {
|
|
|
1322
2374
|
deletedSessionIds,
|
|
1323
2375
|
skippedSessionIds,
|
|
1324
2376
|
skippedLiveSessionIds,
|
|
2377
|
+
skippedCoordinatorSessionIds,
|
|
1325
2378
|
...(deleteUnsupported ? {
|
|
1326
2379
|
deleteUnsupported: true,
|
|
1327
2380
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1429,37 +2482,499 @@ export class DaemonCommandRouter {
|
|
|
1429
2482
|
level: daemonResult.success ? 'info' : 'warn',
|
|
1430
2483
|
payload: { cmd, source: logSource, success: daemonResult.success, durationMs: Date.now() - cmdStart },
|
|
1431
2484
|
});
|
|
1432
|
-
return daemonResult;
|
|
2485
|
+
return daemonResult;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// 2. Delegate to DaemonCommandHandler
|
|
2489
|
+
const handlerResult = await this.deps.commandHandler.handle(cmd, normalizedArgs);
|
|
2490
|
+
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: handlerResult.success, durationMs: Date.now() - cmdStart });
|
|
2491
|
+
recordDebugTrace({
|
|
2492
|
+
interactionId,
|
|
2493
|
+
category: 'command',
|
|
2494
|
+
stage: 'completed',
|
|
2495
|
+
level: handlerResult.success ? 'info' : 'warn',
|
|
2496
|
+
payload: { cmd, source: logSource, success: handlerResult.success, durationMs: Date.now() - cmdStart },
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
// 3. Post-chat command callback
|
|
2500
|
+
if (CHAT_COMMANDS.includes(cmd) && this.deps.onPostChatCommand) {
|
|
2501
|
+
this.deps.onPostChatCommand();
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
return handlerResult;
|
|
2505
|
+
} catch (e: any) {
|
|
2506
|
+
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: false, error: e.message, durationMs: Date.now() - cmdStart });
|
|
2507
|
+
recordDebugTrace({
|
|
2508
|
+
interactionId,
|
|
2509
|
+
category: 'command',
|
|
2510
|
+
stage: 'failed',
|
|
2511
|
+
level: 'error',
|
|
2512
|
+
payload: { cmd, source: logSource, error: e?.message || String(e), durationMs: Date.now() - cmdStart },
|
|
2513
|
+
});
|
|
2514
|
+
throw e;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
|
|
2519
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
2520
|
+
return `${meshId}:${nodeId}`;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
private buildRefineJobHandle(args: {
|
|
2524
|
+
meshId: string;
|
|
2525
|
+
nodeId: string;
|
|
2526
|
+
node?: any;
|
|
2527
|
+
status?: MeshRefineAsyncJobStatus;
|
|
2528
|
+
startedAt?: string;
|
|
2529
|
+
completedAt?: string;
|
|
2530
|
+
jobId?: string;
|
|
2531
|
+
interactionId?: string;
|
|
2532
|
+
retryOfJobId?: string;
|
|
2533
|
+
}): MeshRefineJobHandle {
|
|
2534
|
+
return {
|
|
2535
|
+
success: true,
|
|
2536
|
+
async: true,
|
|
2537
|
+
status: args.status || 'accepted',
|
|
2538
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
2539
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
2540
|
+
meshId: args.meshId,
|
|
2541
|
+
nodeId: args.nodeId,
|
|
2542
|
+
targetNodeId: args.nodeId,
|
|
2543
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
2544
|
+
workspace: readStringValue(args.node?.workspace),
|
|
2545
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
2546
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
2547
|
+
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
2548
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
2549
|
+
evidence: {
|
|
2550
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
2551
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
2552
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
2553
|
+
},
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
2558
|
+
const metadataEvent = {
|
|
2559
|
+
source: 'refine_mesh_node_async_job',
|
|
2560
|
+
jobId: handle.jobId,
|
|
2561
|
+
interactionId: handle.interactionId,
|
|
2562
|
+
meshId: handle.meshId,
|
|
2563
|
+
nodeId: handle.targetNodeId,
|
|
2564
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2565
|
+
workspace: handle.workspace,
|
|
2566
|
+
status: handle.status,
|
|
2567
|
+
startedAt: handle.startedAt,
|
|
2568
|
+
completedAt: handle.completedAt,
|
|
2569
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2570
|
+
...(result ? { result } : {}),
|
|
2571
|
+
};
|
|
2572
|
+
const eventPayload = {
|
|
2573
|
+
event,
|
|
2574
|
+
meshId: handle.meshId,
|
|
2575
|
+
nodeLabel: handle.targetNodeId,
|
|
2576
|
+
nodeId: handle.targetNodeId,
|
|
2577
|
+
workspace: handle.workspace,
|
|
2578
|
+
metadataEvent,
|
|
2579
|
+
queuedAt: Date.now(),
|
|
2580
|
+
};
|
|
2581
|
+
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
2582
|
+
const forwarded = handleMeshForwardEvent(
|
|
2583
|
+
{ instanceManager: this.deps.instanceManager } as any,
|
|
2584
|
+
{
|
|
2585
|
+
event,
|
|
2586
|
+
meshId: handle.meshId,
|
|
2587
|
+
nodeId: handle.targetNodeId,
|
|
2588
|
+
workspace: handle.workspace,
|
|
2589
|
+
jobId: handle.jobId,
|
|
2590
|
+
interactionId: handle.interactionId,
|
|
2591
|
+
status: handle.status,
|
|
2592
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2593
|
+
startedAt: handle.startedAt,
|
|
2594
|
+
completedAt: handle.completedAt,
|
|
2595
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2596
|
+
...(result ? { result } : {}),
|
|
2597
|
+
},
|
|
2598
|
+
);
|
|
2599
|
+
if (forwarded?.success === true) return;
|
|
2600
|
+
LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
|
|
2601
|
+
}
|
|
2602
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
2606
|
+
try {
|
|
2607
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2608
|
+
appendLedgerEntry(handle.meshId, {
|
|
2609
|
+
kind,
|
|
2610
|
+
nodeId: handle.targetNodeId,
|
|
2611
|
+
payload: {
|
|
2612
|
+
source: 'refine_mesh_node_async_job',
|
|
2613
|
+
refineJob: {
|
|
2614
|
+
jobId: handle.jobId,
|
|
2615
|
+
interactionId: handle.interactionId,
|
|
2616
|
+
status: handle.status,
|
|
2617
|
+
meshId: handle.meshId,
|
|
2618
|
+
nodeId: handle.targetNodeId,
|
|
2619
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2620
|
+
workspace: handle.workspace,
|
|
2621
|
+
startedAt: handle.startedAt,
|
|
2622
|
+
completedAt: handle.completedAt,
|
|
2623
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2624
|
+
},
|
|
2625
|
+
async: true,
|
|
2626
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2627
|
+
...(result ? {
|
|
2628
|
+
success: result.success === true,
|
|
2629
|
+
result,
|
|
2630
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
2631
|
+
} : {}),
|
|
2632
|
+
},
|
|
2633
|
+
});
|
|
2634
|
+
} catch (e: any) {
|
|
2635
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2640
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
2641
|
+
try {
|
|
2642
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2643
|
+
const mesh = meshRecord?.mesh;
|
|
2644
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2645
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
2646
|
+
|
|
2647
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
2648
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
const sourceNode = node.clonedFromNodeId
|
|
2652
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2653
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2654
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2655
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
2656
|
+
|
|
2657
|
+
const { execFile } = await import('node:child_process');
|
|
2658
|
+
const { promisify } = await import('node:util');
|
|
2659
|
+
const execFileAsync = promisify(execFile);
|
|
2660
|
+
|
|
2661
|
+
const resolveStarted = Date.now();
|
|
2662
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2663
|
+
const branch = branchStdout.trim();
|
|
2664
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
2665
|
+
|
|
2666
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2667
|
+
const baseBranch = baseBranchStdout.trim();
|
|
2668
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2669
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
2670
|
+
const baseHead = baseHeadStdout.trim();
|
|
2671
|
+
const branchHead = branchHeadStdout.trim();
|
|
2672
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
2673
|
+
|
|
2674
|
+
const validationStarted = Date.now();
|
|
2675
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2676
|
+
recordMeshRefineStage(
|
|
2677
|
+
refineStages,
|
|
2678
|
+
'validation',
|
|
2679
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
2680
|
+
validationStarted,
|
|
2681
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
2682
|
+
);
|
|
2683
|
+
if (validationSummary.status === 'failed') {
|
|
2684
|
+
return {
|
|
2685
|
+
success: false,
|
|
2686
|
+
code: 'validation_failed',
|
|
2687
|
+
convergenceStatus: 'blocked_review',
|
|
2688
|
+
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2689
|
+
branch,
|
|
2690
|
+
into: baseBranch,
|
|
2691
|
+
validationSummary,
|
|
2692
|
+
refineStages,
|
|
2693
|
+
finalBranchConvergenceState: {
|
|
2694
|
+
branch,
|
|
2695
|
+
baseBranch,
|
|
2696
|
+
merged: false,
|
|
2697
|
+
removed: false,
|
|
2698
|
+
validation: 'failed',
|
|
2699
|
+
status: 'blocked_review',
|
|
2700
|
+
},
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
if (validationSummary.status === 'skipped') {
|
|
2704
|
+
return {
|
|
2705
|
+
success: false,
|
|
2706
|
+
code: 'validation_unavailable',
|
|
2707
|
+
convergenceStatus: 'blocked_review',
|
|
2708
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2709
|
+
branch,
|
|
2710
|
+
into: baseBranch,
|
|
2711
|
+
validationSummary,
|
|
2712
|
+
refineStages,
|
|
2713
|
+
finalBranchConvergenceState: {
|
|
2714
|
+
branch,
|
|
2715
|
+
baseBranch,
|
|
2716
|
+
merged: false,
|
|
2717
|
+
removed: false,
|
|
2718
|
+
validation: 'unavailable',
|
|
2719
|
+
status: 'blocked_review',
|
|
2720
|
+
},
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
const patchEquivalenceStarted = Date.now();
|
|
2725
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
2726
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
2727
|
+
equivalent: patchEquivalence.equivalent,
|
|
2728
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
2729
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
2730
|
+
error: patchEquivalence.error,
|
|
2731
|
+
});
|
|
2732
|
+
if (!patchEquivalence.equivalent) {
|
|
2733
|
+
return {
|
|
2734
|
+
success: false,
|
|
2735
|
+
code: 'patch_equivalence_failed',
|
|
2736
|
+
convergenceStatus: 'blocked_review',
|
|
2737
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
2738
|
+
branch,
|
|
2739
|
+
into: baseBranch,
|
|
2740
|
+
validationSummary,
|
|
2741
|
+
patchEquivalence,
|
|
2742
|
+
refineStages,
|
|
2743
|
+
finalBranchConvergenceState: {
|
|
2744
|
+
branch,
|
|
2745
|
+
baseBranch,
|
|
2746
|
+
merged: false,
|
|
2747
|
+
removed: false,
|
|
2748
|
+
validation: 'passed',
|
|
2749
|
+
patchEquivalence: 'failed',
|
|
2750
|
+
status: 'blocked_review',
|
|
2751
|
+
},
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
const submoduleReachabilityStarted = Date.now();
|
|
2756
|
+
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead);
|
|
2757
|
+
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
2758
|
+
checked: submoduleReachability.checked,
|
|
2759
|
+
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
2760
|
+
path: entry.path,
|
|
2761
|
+
commit: entry.commit,
|
|
2762
|
+
publishRequired: entry.publishRequired === true,
|
|
2763
|
+
remote: entry.remote,
|
|
2764
|
+
remoteUrl: entry.remoteUrl,
|
|
2765
|
+
remoteReachable: entry.remoteReachable,
|
|
2766
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
2767
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
2768
|
+
error: entry.error,
|
|
2769
|
+
})),
|
|
2770
|
+
error: submoduleReachability.error,
|
|
2771
|
+
});
|
|
2772
|
+
if (submoduleReachability.status === 'failed') {
|
|
2773
|
+
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
2774
|
+
return {
|
|
2775
|
+
success: false,
|
|
2776
|
+
code: 'submodule_reachability_failed',
|
|
2777
|
+
convergenceStatus: 'blocked_review',
|
|
2778
|
+
publishRequired: true,
|
|
2779
|
+
blockedReason: 'submodule_publish_required',
|
|
2780
|
+
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.',
|
|
2781
|
+
nextStep,
|
|
2782
|
+
nextSteps: [
|
|
2783
|
+
'Ask the user for explicit approval before pushing or publishing any submodule commit.',
|
|
2784
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
|
|
2785
|
+
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
2786
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
|
|
2787
|
+
],
|
|
2788
|
+
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
2789
|
+
path: entry.path,
|
|
2790
|
+
commit: entry.commit,
|
|
2791
|
+
remote: entry.remote,
|
|
2792
|
+
remoteUrl: entry.remoteUrl,
|
|
2793
|
+
remoteReachable: entry.remoteReachable,
|
|
2794
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
2795
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
2796
|
+
error: entry.error,
|
|
2797
|
+
})),
|
|
2798
|
+
branch,
|
|
2799
|
+
into: baseBranch,
|
|
2800
|
+
validationSummary,
|
|
2801
|
+
patchEquivalence,
|
|
2802
|
+
submoduleReachability,
|
|
2803
|
+
refineStages,
|
|
2804
|
+
finalBranchConvergenceState: {
|
|
2805
|
+
branch,
|
|
2806
|
+
baseBranch,
|
|
2807
|
+
merged: false,
|
|
2808
|
+
removed: false,
|
|
2809
|
+
validation: 'passed',
|
|
2810
|
+
patchEquivalence: 'passed',
|
|
2811
|
+
submoduleReachability: 'failed',
|
|
2812
|
+
status: 'blocked_review',
|
|
2813
|
+
reason: 'submodule_publish_required',
|
|
2814
|
+
nextStep,
|
|
2815
|
+
},
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
2820
|
+
const mergeStarted = Date.now();
|
|
2821
|
+
try {
|
|
2822
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2823
|
+
mergeResult = {
|
|
2824
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
2825
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
2826
|
+
durationMs: Date.now() - mergeStarted,
|
|
2827
|
+
};
|
|
2828
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
2829
|
+
} catch (e: any) {
|
|
2830
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
2831
|
+
error: e?.message || String(e),
|
|
2832
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
2833
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
2834
|
+
});
|
|
2835
|
+
return {
|
|
2836
|
+
success: false,
|
|
2837
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2838
|
+
validationSummary,
|
|
2839
|
+
patchEquivalence,
|
|
2840
|
+
refineStages,
|
|
2841
|
+
finalBranchConvergenceState: {
|
|
2842
|
+
branch,
|
|
2843
|
+
baseBranch,
|
|
2844
|
+
merged: false,
|
|
2845
|
+
removed: false,
|
|
2846
|
+
validation: 'passed',
|
|
2847
|
+
patchEquivalence: 'passed',
|
|
2848
|
+
status: 'not_mergeable',
|
|
2849
|
+
},
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
const cleanupStarted = Date.now();
|
|
2854
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
2855
|
+
meshId,
|
|
2856
|
+
nodeId,
|
|
2857
|
+
sessionCleanupMode: 'preserve',
|
|
2858
|
+
inlineMesh: args?.inlineMesh,
|
|
2859
|
+
});
|
|
2860
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
2861
|
+
removed: removeResult?.removed,
|
|
2862
|
+
code: removeResult?.code,
|
|
2863
|
+
error: removeResult?.error,
|
|
2864
|
+
});
|
|
2865
|
+
|
|
2866
|
+
let ledgerError: string | undefined;
|
|
2867
|
+
const ledgerStarted = Date.now();
|
|
2868
|
+
try {
|
|
2869
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2870
|
+
appendLedgerEntry(meshId, {
|
|
2871
|
+
kind: 'node_removed',
|
|
2872
|
+
nodeId,
|
|
2873
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
2874
|
+
});
|
|
2875
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
2876
|
+
} catch (e: any) {
|
|
2877
|
+
ledgerError = e?.message || String(e);
|
|
2878
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
1433
2879
|
}
|
|
1434
2880
|
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
}
|
|
2881
|
+
const finalBranchConvergenceState = {
|
|
2882
|
+
branch: baseBranch,
|
|
2883
|
+
mergedBranch: branch,
|
|
2884
|
+
baseBranch,
|
|
2885
|
+
merged: true,
|
|
2886
|
+
removed: removeResult?.success !== false,
|
|
2887
|
+
validation: 'passed',
|
|
2888
|
+
patchEquivalence: 'passed',
|
|
2889
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2890
|
+
};
|
|
1445
2891
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
2892
|
+
if (removeResult?.success === false) {
|
|
2893
|
+
return {
|
|
2894
|
+
success: false,
|
|
2895
|
+
code: 'cleanup_failed',
|
|
2896
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
2897
|
+
merged: true,
|
|
2898
|
+
branch,
|
|
2899
|
+
into: baseBranch,
|
|
2900
|
+
removeResult,
|
|
2901
|
+
validationSummary,
|
|
2902
|
+
patchEquivalence,
|
|
2903
|
+
mergeResult,
|
|
2904
|
+
refineStages,
|
|
2905
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2906
|
+
finalBranchConvergenceState,
|
|
2907
|
+
};
|
|
1449
2908
|
}
|
|
1450
2909
|
|
|
1451
|
-
return
|
|
2910
|
+
return {
|
|
2911
|
+
success: true,
|
|
2912
|
+
merged: true,
|
|
2913
|
+
branch,
|
|
2914
|
+
into: baseBranch,
|
|
2915
|
+
removeResult,
|
|
2916
|
+
validationSummary,
|
|
2917
|
+
patchEquivalence,
|
|
2918
|
+
mergeResult,
|
|
2919
|
+
refineStages,
|
|
2920
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2921
|
+
finalBranchConvergenceState,
|
|
2922
|
+
};
|
|
1452
2923
|
} catch (e: any) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
2924
|
+
return { success: false, error: e.message, refineStages };
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
2929
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
2930
|
+
let result: Record<string, unknown>;
|
|
2931
|
+
try {
|
|
2932
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
2933
|
+
} catch (e: any) {
|
|
2934
|
+
result = { success: false, error: e?.message || String(e) };
|
|
1462
2935
|
}
|
|
2936
|
+
const completedAt = new Date().toISOString();
|
|
2937
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
2938
|
+
meshId: handle.meshId,
|
|
2939
|
+
nodeId: handle.targetNodeId,
|
|
2940
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
2941
|
+
startedAt: handle.startedAt,
|
|
2942
|
+
completedAt,
|
|
2943
|
+
jobId: handle.jobId,
|
|
2944
|
+
interactionId: handle.interactionId,
|
|
2945
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2946
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
2947
|
+
});
|
|
2948
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
2949
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
2950
|
+
this.runningRefineJobs.delete(key);
|
|
2951
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
2952
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
2953
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2957
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
2958
|
+
const running = this.runningRefineJobs.get(key);
|
|
2959
|
+
if (running) return { ...running, duplicate: true };
|
|
2960
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
2961
|
+
|
|
2962
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2963
|
+
const mesh = meshRecord?.mesh;
|
|
2964
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2965
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
2966
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
2967
|
+
|
|
2968
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
|
|
2969
|
+
this.runningRefineJobs.set(key, handle);
|
|
2970
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
2971
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
2972
|
+
|
|
2973
|
+
setImmediate(() => {
|
|
2974
|
+
void this.finishMeshRefineJob(handle, args);
|
|
2975
|
+
});
|
|
2976
|
+
|
|
2977
|
+
return handle;
|
|
1463
2978
|
}
|
|
1464
2979
|
|
|
1465
2980
|
// ─── Daemon-level command core ───────────────────
|
|
@@ -1476,7 +2991,8 @@ export class DaemonCommandRouter {
|
|
|
1476
2991
|
}
|
|
1477
2992
|
|
|
1478
2993
|
case 'get_pending_mesh_events': {
|
|
1479
|
-
const
|
|
2994
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2995
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1480
2996
|
return { success: true, events };
|
|
1481
2997
|
}
|
|
1482
2998
|
|
|
@@ -2081,15 +3597,44 @@ export class DaemonCommandRouter {
|
|
|
2081
3597
|
case 'get_mesh': {
|
|
2082
3598
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2083
3599
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
3600
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3601
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
3602
|
+
|
|
3603
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
3604
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
3605
|
+
mesh: meshRecord.mesh,
|
|
3606
|
+
meshSource: meshRecord.source,
|
|
3607
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
3608
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
3609
|
+
localMachineId: loadConfig().machineId || '',
|
|
3610
|
+
});
|
|
3611
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
3612
|
+
const sourceOfTruth = {
|
|
3613
|
+
membership: meshRecord.source === 'inline_cache'
|
|
3614
|
+
? 'coordinator_inline_mesh_cache'
|
|
3615
|
+
: meshRecord.source === 'local_config'
|
|
3616
|
+
? 'local_mesh_config'
|
|
3617
|
+
: 'inline_bootstrap_snapshot',
|
|
3618
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
3619
|
+
directPeerTruth: {
|
|
3620
|
+
required: requireDirectPeerTruth,
|
|
3621
|
+
satisfied: directTruthSatisfied,
|
|
3622
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
3623
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
3624
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
3625
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
3626
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
3627
|
+
},
|
|
3628
|
+
};
|
|
3629
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
3630
|
+
return {
|
|
3631
|
+
success: false,
|
|
3632
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
3633
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
3634
|
+
sourceOfTruth,
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
2093
3638
|
}
|
|
2094
3639
|
|
|
2095
3640
|
case 'create_mesh': {
|
|
@@ -2100,7 +3645,10 @@ export class DaemonCommandRouter {
|
|
|
2100
3645
|
if (!name) return { success: false, error: 'name required' };
|
|
2101
3646
|
try {
|
|
2102
3647
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
2103
|
-
const
|
|
3648
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
3649
|
+
? args.meshHost
|
|
3650
|
+
: undefined;
|
|
3651
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
2104
3652
|
return { success: true, mesh };
|
|
2105
3653
|
} catch (e: any) {
|
|
2106
3654
|
return { success: false, error: e.message };
|
|
@@ -2117,16 +3665,237 @@ export class DaemonCommandRouter {
|
|
|
2117
3665
|
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
2118
3666
|
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
2119
3667
|
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
3668
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
2120
3669
|
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
2121
3670
|
const mesh = updateMesh(meshId, patch as any);
|
|
2122
3671
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2123
3672
|
this.inlineMeshCache.set(meshId, mesh);
|
|
3673
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2124
3674
|
return { success: true, mesh };
|
|
2125
3675
|
} catch (e: any) {
|
|
2126
3676
|
return { success: false, error: e.message };
|
|
2127
3677
|
}
|
|
2128
3678
|
}
|
|
2129
3679
|
|
|
3680
|
+
case 'get_mesh_host_pairing': {
|
|
3681
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3682
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3683
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3684
|
+
const mesh = meshRecord?.mesh;
|
|
3685
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3686
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3687
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
3688
|
+
return {
|
|
3689
|
+
success: true,
|
|
3690
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
3691
|
+
meshId,
|
|
3692
|
+
hostAddress: meshHost.hostAddress,
|
|
3693
|
+
meshHost,
|
|
3694
|
+
manualPairing: {
|
|
3695
|
+
status: pairingStatus,
|
|
3696
|
+
joinImplemented: true,
|
|
3697
|
+
protocol: 'standalone_command_direct_v1',
|
|
3698
|
+
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.',
|
|
3699
|
+
},
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
case 'configure_mesh_host_pairing': {
|
|
3704
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3705
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
3706
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3707
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3708
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
3709
|
+
try {
|
|
3710
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
3711
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
3712
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
3713
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
3714
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
3715
|
+
return {
|
|
3716
|
+
success: true,
|
|
3717
|
+
code: 'mesh_host_pairing_pending',
|
|
3718
|
+
meshId,
|
|
3719
|
+
hostAddress: configured.hostAddress,
|
|
3720
|
+
meshHost,
|
|
3721
|
+
manualPairing: {
|
|
3722
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
3723
|
+
joinImplemented: true,
|
|
3724
|
+
protocol: 'standalone_command_direct_v1',
|
|
3725
|
+
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.',
|
|
3726
|
+
},
|
|
3727
|
+
};
|
|
3728
|
+
} catch (e: any) {
|
|
3729
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
case 'create_mesh_host_pairing_token': {
|
|
3734
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3735
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3736
|
+
try {
|
|
3737
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
3738
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
3739
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
3740
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
3741
|
+
});
|
|
3742
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
3743
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
3744
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3745
|
+
return {
|
|
3746
|
+
success: true,
|
|
3747
|
+
code: 'mesh_host_pairing_token_created',
|
|
3748
|
+
meshId,
|
|
3749
|
+
token: created.token,
|
|
3750
|
+
tokenId: created.tokenId,
|
|
3751
|
+
expiresAt: created.expiresAt,
|
|
3752
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
3753
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
3754
|
+
};
|
|
3755
|
+
} catch (e: any) {
|
|
3756
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
case 'apply_mesh_host_join': {
|
|
3761
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3762
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3763
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
3764
|
+
? args.memberNode
|
|
3765
|
+
: null;
|
|
3766
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3767
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
3768
|
+
try {
|
|
3769
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
3770
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
3771
|
+
token,
|
|
3772
|
+
memberNode: memberNode as any,
|
|
3773
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
3774
|
+
});
|
|
3775
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
3776
|
+
if (!applied.accepted) {
|
|
3777
|
+
return {
|
|
3778
|
+
success: false,
|
|
3779
|
+
code: 'mesh_host_join_rejected',
|
|
3780
|
+
meshId,
|
|
3781
|
+
tokenId: applied.tokenId,
|
|
3782
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
3783
|
+
error: applied.reason,
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
3787
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3788
|
+
try {
|
|
3789
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3790
|
+
appendLedgerEntry(meshId, {
|
|
3791
|
+
kind: 'node_joined',
|
|
3792
|
+
nodeId: applied.node.id,
|
|
3793
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
3794
|
+
});
|
|
3795
|
+
} catch { /* ledger append is best-effort */ }
|
|
3796
|
+
return {
|
|
3797
|
+
success: true,
|
|
3798
|
+
code: 'mesh_host_join_accepted',
|
|
3799
|
+
meshId,
|
|
3800
|
+
node: applied.node,
|
|
3801
|
+
tokenId: applied.tokenId,
|
|
3802
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
3803
|
+
};
|
|
3804
|
+
} catch (e: any) {
|
|
3805
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
case 'join_mesh_host_pairing': {
|
|
3810
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3811
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3812
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3813
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
3814
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3815
|
+
const mesh = meshRecord?.mesh;
|
|
3816
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3817
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3818
|
+
if (meshHost.role !== 'member') {
|
|
3819
|
+
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.' };
|
|
3820
|
+
}
|
|
3821
|
+
try {
|
|
3822
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
3823
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
3824
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
3825
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
3826
|
+
}
|
|
3827
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
3828
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
3829
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
3830
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
3831
|
+
? args.hostDaemonId.trim()
|
|
3832
|
+
: meshHost.hostDaemonId;
|
|
3833
|
+
let hostResult: any;
|
|
3834
|
+
let transport: string;
|
|
3835
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
3836
|
+
transport = 'mesh_command_dispatch';
|
|
3837
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
3838
|
+
meshId: hostMeshId,
|
|
3839
|
+
token,
|
|
3840
|
+
memberMeshId: meshId,
|
|
3841
|
+
memberNode,
|
|
3842
|
+
});
|
|
3843
|
+
} else if (meshHost.hostAddress) {
|
|
3844
|
+
transport = 'standalone_http_command';
|
|
3845
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
3846
|
+
const response = await fetch(commandUrl, {
|
|
3847
|
+
method: 'POST',
|
|
3848
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3849
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
3850
|
+
});
|
|
3851
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
3852
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
3853
|
+
} else {
|
|
3854
|
+
return {
|
|
3855
|
+
success: false,
|
|
3856
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
3857
|
+
meshId,
|
|
3858
|
+
meshHost,
|
|
3859
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
if (!hostResult?.success) {
|
|
3863
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
3864
|
+
}
|
|
3865
|
+
const joined = meshRecord.inline
|
|
3866
|
+
? null
|
|
3867
|
+
: markMeshHostPairingJoined(meshId, {
|
|
3868
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3869
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
3870
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
3871
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
3872
|
+
});
|
|
3873
|
+
if (joined) {
|
|
3874
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
3875
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3876
|
+
}
|
|
3877
|
+
return {
|
|
3878
|
+
success: true,
|
|
3879
|
+
code: 'mesh_host_join_applied',
|
|
3880
|
+
meshId,
|
|
3881
|
+
hostMeshId,
|
|
3882
|
+
transport,
|
|
3883
|
+
node: hostResult.node,
|
|
3884
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3885
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
3886
|
+
hostResult,
|
|
3887
|
+
manualPairing: {
|
|
3888
|
+
status: 'paired',
|
|
3889
|
+
joinImplemented: true,
|
|
3890
|
+
protocol: 'standalone_command_direct_v1',
|
|
3891
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
3892
|
+
},
|
|
3893
|
+
};
|
|
3894
|
+
} catch (e: any) {
|
|
3895
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
|
|
2130
3899
|
case 'delete_mesh': {
|
|
2131
3900
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2132
3901
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -2220,6 +3989,8 @@ export class DaemonCommandRouter {
|
|
|
2220
3989
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2221
3990
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2222
3991
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3992
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
3993
|
+
if (ownerFailure) return ownerFailure;
|
|
2223
3994
|
try {
|
|
2224
3995
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2225
3996
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -2235,6 +4006,8 @@ export class DaemonCommandRouter {
|
|
|
2235
4006
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2236
4007
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2237
4008
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4009
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
4010
|
+
if (ownerFailure) return ownerFailure;
|
|
2238
4011
|
try {
|
|
2239
4012
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2240
4013
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -2256,6 +4029,8 @@ export class DaemonCommandRouter {
|
|
|
2256
4029
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2257
4030
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2258
4031
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
4032
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
4033
|
+
if (ownerFailure) return ownerFailure;
|
|
2259
4034
|
try {
|
|
2260
4035
|
const { addNode } = await import('../config/mesh-config.js');
|
|
2261
4036
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -2266,7 +4041,18 @@ export class DaemonCommandRouter {
|
|
|
2266
4041
|
...(readOnly ? { readOnly: true } : {}),
|
|
2267
4042
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
2268
4043
|
};
|
|
2269
|
-
const
|
|
4044
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
4045
|
+
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
4046
|
+
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
4047
|
+
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
4048
|
+
const node = addNode(meshId, {
|
|
4049
|
+
workspace,
|
|
4050
|
+
...(repoRoot ? { repoRoot } : {}),
|
|
4051
|
+
...(daemonId ? { daemonId } : {}),
|
|
4052
|
+
...(machineId ? { machineId } : {}),
|
|
4053
|
+
...(policy ? { policy } : {}),
|
|
4054
|
+
...(role ? { role } : {}),
|
|
4055
|
+
});
|
|
2270
4056
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
2271
4057
|
return { success: true, node };
|
|
2272
4058
|
} catch (e: any) {
|
|
@@ -2278,6 +4064,8 @@ export class DaemonCommandRouter {
|
|
|
2278
4064
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2279
4065
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2280
4066
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4067
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
4068
|
+
if (ownerFailure) return ownerFailure;
|
|
2281
4069
|
try {
|
|
2282
4070
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2283
4071
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2306,6 +4094,8 @@ export class DaemonCommandRouter {
|
|
|
2306
4094
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2307
4095
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2308
4096
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4097
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
4098
|
+
if (ownerFailure) return ownerFailure;
|
|
2309
4099
|
try {
|
|
2310
4100
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2311
4101
|
const mesh = meshRecord?.mesh;
|
|
@@ -2331,131 +4121,91 @@ export class DaemonCommandRouter {
|
|
|
2331
4121
|
}
|
|
2332
4122
|
}
|
|
2333
4123
|
|
|
2334
|
-
case '
|
|
4124
|
+
case 'get_mesh_refine_config_schema': {
|
|
4125
|
+
return {
|
|
4126
|
+
success: true,
|
|
4127
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
4128
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
4129
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
4130
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
case 'validate_mesh_refine_config': {
|
|
4135
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4136
|
+
const mesh = args?.inlineMesh || {};
|
|
4137
|
+
const loaded = args?.config !== undefined
|
|
4138
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
4139
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
4140
|
+
const validation = loaded.config
|
|
4141
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
4142
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
4143
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
case 'suggest_mesh_refine_config': {
|
|
4147
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4148
|
+
const mesh = args?.inlineMesh || {};
|
|
4149
|
+
return {
|
|
4150
|
+
success: true,
|
|
4151
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
4152
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
case 'plan_mesh_refine_node': {
|
|
2335
4157
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2336
4158
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2337
4159
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2338
|
-
|
|
4160
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
4161
|
+
const mesh = meshRecord?.mesh;
|
|
4162
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
4163
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
4164
|
+
return {
|
|
4165
|
+
success: true,
|
|
4166
|
+
dryRun: true,
|
|
4167
|
+
nodeId,
|
|
4168
|
+
workspace: node.workspace,
|
|
4169
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
4170
|
+
mergeWillRun: false,
|
|
4171
|
+
cleanupWillRun: false,
|
|
4172
|
+
};
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
case 'fast_forward_mesh_node': {
|
|
4176
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4177
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4178
|
+
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
4179
|
+
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
4180
|
+
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
4181
|
+
: undefined;
|
|
4182
|
+
if (!workspace && meshId && nodeId) {
|
|
2339
4183
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2340
4184
|
const mesh = meshRecord?.mesh;
|
|
2341
4185
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
return { success: false, error: `Refinery requires a local worktree node` };
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
const sourceNode = node.clonedFromNodeId
|
|
2349
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2350
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2351
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2352
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2353
|
-
|
|
2354
|
-
const { execFile } = await import('node:child_process');
|
|
2355
|
-
const { promisify } = await import('node:util');
|
|
2356
|
-
const execFileAsync = promisify(execFile);
|
|
2357
|
-
|
|
2358
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2359
|
-
const branch = branchStdout.trim();
|
|
2360
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2361
|
-
|
|
2362
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2363
|
-
const baseBranch = baseBranchStdout.trim();
|
|
2364
|
-
|
|
2365
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2366
|
-
if (validationSummary.status === 'failed') {
|
|
2367
|
-
return {
|
|
2368
|
-
success: false,
|
|
2369
|
-
code: 'validation_failed',
|
|
2370
|
-
convergenceStatus: 'blocked_review',
|
|
2371
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2372
|
-
branch,
|
|
2373
|
-
into: baseBranch,
|
|
2374
|
-
validationSummary,
|
|
2375
|
-
finalBranchConvergenceState: {
|
|
2376
|
-
branch,
|
|
2377
|
-
baseBranch,
|
|
2378
|
-
merged: false,
|
|
2379
|
-
removed: false,
|
|
2380
|
-
validation: 'failed',
|
|
2381
|
-
status: 'blocked_review',
|
|
2382
|
-
},
|
|
2383
|
-
};
|
|
2384
|
-
}
|
|
2385
|
-
if (validationSummary.status === 'skipped') {
|
|
2386
|
-
return {
|
|
2387
|
-
success: false,
|
|
2388
|
-
code: 'validation_unavailable',
|
|
2389
|
-
convergenceStatus: 'blocked_review',
|
|
2390
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2391
|
-
branch,
|
|
2392
|
-
into: baseBranch,
|
|
2393
|
-
validationSummary,
|
|
2394
|
-
finalBranchConvergenceState: {
|
|
2395
|
-
branch,
|
|
2396
|
-
baseBranch,
|
|
2397
|
-
merged: false,
|
|
2398
|
-
removed: false,
|
|
2399
|
-
validation: 'unavailable',
|
|
2400
|
-
status: 'blocked_review',
|
|
2401
|
-
},
|
|
2402
|
-
};
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
try {
|
|
2406
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2407
|
-
} catch (e: any) {
|
|
2408
|
-
return {
|
|
2409
|
-
success: false,
|
|
2410
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2411
|
-
validationSummary,
|
|
2412
|
-
finalBranchConvergenceState: {
|
|
2413
|
-
branch,
|
|
2414
|
-
baseBranch,
|
|
2415
|
-
merged: false,
|
|
2416
|
-
removed: false,
|
|
2417
|
-
validation: 'passed',
|
|
2418
|
-
status: 'not_mergeable',
|
|
2419
|
-
},
|
|
2420
|
-
};
|
|
4186
|
+
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
4187
|
+
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
4188
|
+
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
2421
4189
|
}
|
|
2422
|
-
|
|
2423
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
2424
|
-
meshId,
|
|
2425
|
-
nodeId,
|
|
2426
|
-
sessionCleanupMode: 'kill',
|
|
2427
|
-
inlineMesh: args?.inlineMesh,
|
|
2428
|
-
});
|
|
2429
|
-
|
|
2430
|
-
try {
|
|
2431
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2432
|
-
appendLedgerEntry(meshId, {
|
|
2433
|
-
kind: 'node_removed',
|
|
2434
|
-
nodeId,
|
|
2435
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
2436
|
-
});
|
|
2437
|
-
} catch {}
|
|
2438
|
-
|
|
2439
|
-
return {
|
|
2440
|
-
success: true,
|
|
2441
|
-
merged: true,
|
|
2442
|
-
branch,
|
|
2443
|
-
into: baseBranch,
|
|
2444
|
-
removeResult,
|
|
2445
|
-
validationSummary,
|
|
2446
|
-
finalBranchConvergenceState: {
|
|
2447
|
-
branch: baseBranch,
|
|
2448
|
-
mergedBranch: branch,
|
|
2449
|
-
baseBranch,
|
|
2450
|
-
merged: true,
|
|
2451
|
-
removed: removeResult?.success !== false,
|
|
2452
|
-
validation: 'passed',
|
|
2453
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2454
|
-
},
|
|
2455
|
-
};
|
|
2456
|
-
} catch (e: any) {
|
|
2457
|
-
return { success: false, error: e.message };
|
|
2458
4190
|
}
|
|
4191
|
+
const result = await (fastForwardMeshNode({
|
|
4192
|
+
meshId: meshId || undefined,
|
|
4193
|
+
nodeId: nodeId || undefined,
|
|
4194
|
+
workspace,
|
|
4195
|
+
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
4196
|
+
execute: args?.execute === true,
|
|
4197
|
+
dryRun: args?.dryRun === true,
|
|
4198
|
+
updateSubmodules: args?.updateSubmodules === true,
|
|
4199
|
+
submoduleIgnorePaths,
|
|
4200
|
+
}) as Promise<unknown>);
|
|
4201
|
+
return result as CommandRouterResult;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
case 'refine_mesh_node': {
|
|
4205
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4206
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4207
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4208
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
2459
4209
|
}
|
|
2460
4210
|
|
|
2461
4211
|
case 'remove_mesh_node': {
|
|
@@ -2499,6 +4249,7 @@ export class DaemonCommandRouter {
|
|
|
2499
4249
|
} else {
|
|
2500
4250
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2501
4251
|
removed = removeNode(meshId, nodeId);
|
|
4252
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2502
4253
|
}
|
|
2503
4254
|
|
|
2504
4255
|
// Record in task ledger
|
|
@@ -2536,6 +4287,8 @@ export class DaemonCommandRouter {
|
|
|
2536
4287
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2537
4288
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2538
4289
|
if (!branch) return { success: false, error: 'branch required' };
|
|
4290
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
4291
|
+
if (ownerFailure) return ownerFailure;
|
|
2539
4292
|
|
|
2540
4293
|
try {
|
|
2541
4294
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2584,6 +4337,7 @@ export class DaemonCommandRouter {
|
|
|
2584
4337
|
policy: { ...(sourceNode.policy || {}) },
|
|
2585
4338
|
});
|
|
2586
4339
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
4340
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2587
4341
|
}
|
|
2588
4342
|
|
|
2589
4343
|
// Initialize submodules if policy allows (default: true)
|
|
@@ -2625,6 +4379,8 @@ export class DaemonCommandRouter {
|
|
|
2625
4379
|
case 'trigger_mesh_queue': {
|
|
2626
4380
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2627
4381
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4382
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
4383
|
+
if (ownerFailure) return ownerFailure;
|
|
2628
4384
|
try {
|
|
2629
4385
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2630
4386
|
if (meshId) {
|
|
@@ -2656,6 +4412,15 @@ export class DaemonCommandRouter {
|
|
|
2656
4412
|
mesh = getMesh(meshId);
|
|
2657
4413
|
}
|
|
2658
4414
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4415
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4416
|
+
if (!meshHost.canOwnCoordinator) {
|
|
4417
|
+
return {
|
|
4418
|
+
success: false,
|
|
4419
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
4420
|
+
meshId,
|
|
4421
|
+
cliType,
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
2659
4424
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2660
4425
|
|
|
2661
4426
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2675,7 +4440,16 @@ export class DaemonCommandRouter {
|
|
|
2675
4440
|
cliType,
|
|
2676
4441
|
};
|
|
2677
4442
|
}
|
|
2678
|
-
const
|
|
4443
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
4444
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
4445
|
+
: [];
|
|
4446
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
4447
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4448
|
+
meshId,
|
|
4449
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
4450
|
+
liveSessionRecords: liveMeshSessions,
|
|
4451
|
+
allowCoordinatorSession: true,
|
|
4452
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2679
4453
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2680
4454
|
if (!cliType) {
|
|
2681
4455
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -3020,6 +4794,27 @@ export class DaemonCommandRouter {
|
|
|
3020
4794
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3021
4795
|
const mesh = meshRecord?.mesh;
|
|
3022
4796
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4797
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4798
|
+
|
|
4799
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
4800
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
4801
|
+
if (!refreshRequested) {
|
|
4802
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
4803
|
+
if (cachedStatus) {
|
|
4804
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
4805
|
+
meshId,
|
|
4806
|
+
command: 'mesh_status',
|
|
4807
|
+
refreshRequested,
|
|
4808
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
4809
|
+
});
|
|
4810
|
+
return cachedStatus;
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
const refreshReason = refreshRequested
|
|
4814
|
+
? 'explicit_refresh'
|
|
4815
|
+
: hadAggregateCache
|
|
4816
|
+
? 'stale_pending_cache_refresh'
|
|
4817
|
+
: 'cold_cache_miss';
|
|
3023
4818
|
|
|
3024
4819
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
3025
4820
|
const queue = getQueue(meshId);
|
|
@@ -3034,8 +4829,74 @@ export class DaemonCommandRouter {
|
|
|
3034
4829
|
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
3035
4830
|
|
|
3036
4831
|
const localMachineId = loadConfig().machineId || '';
|
|
4832
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
4833
|
+
const directTruth = requireDirectPeerTruth
|
|
4834
|
+
? await hydrateInlineMeshDirectTruth({
|
|
4835
|
+
mesh,
|
|
4836
|
+
meshSource: meshRecord.source,
|
|
4837
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4838
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
4839
|
+
localMachineId,
|
|
4840
|
+
})
|
|
4841
|
+
: {
|
|
4842
|
+
directEvidenceCount: 0,
|
|
4843
|
+
localConfirmedCount: 0,
|
|
4844
|
+
peerAttemptedCount: 0,
|
|
4845
|
+
peerConfirmedCount: 0,
|
|
4846
|
+
unavailableNodeIds: [] as string[],
|
|
4847
|
+
};
|
|
4848
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
4849
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
4850
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
4851
|
+
&& !refreshRequested
|
|
4852
|
+
&& directTruth.directEvidenceCount > 0
|
|
4853
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
4854
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
4855
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
4856
|
+
: directTruth;
|
|
4857
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
4858
|
+
|| effectiveDirectTruth.directEvidenceCount > 0;
|
|
4859
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4860
|
+
const failureResult = {
|
|
4861
|
+
success: false,
|
|
4862
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
4863
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
4864
|
+
sourceOfTruth: {
|
|
4865
|
+
membership: meshRecord.source === 'inline_cache'
|
|
4866
|
+
? 'coordinator_inline_mesh_cache'
|
|
4867
|
+
: meshRecord.source === 'local_config'
|
|
4868
|
+
? 'local_mesh_config'
|
|
4869
|
+
: 'inline_bootstrap_snapshot',
|
|
4870
|
+
coordinatorOwnsLiveTruth: false,
|
|
4871
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
4872
|
+
directPeerTruth: {
|
|
4873
|
+
required: true,
|
|
4874
|
+
satisfied: false,
|
|
4875
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4876
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4877
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4878
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4879
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4880
|
+
},
|
|
4881
|
+
},
|
|
4882
|
+
};
|
|
4883
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
4884
|
+
meshId,
|
|
4885
|
+
command: 'mesh_status',
|
|
4886
|
+
refreshRequested,
|
|
4887
|
+
meshSource: meshRecord.source,
|
|
4888
|
+
directTruth,
|
|
4889
|
+
});
|
|
4890
|
+
return failureResult;
|
|
4891
|
+
}
|
|
4892
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
4893
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
4894
|
+
mesh.coordinator?.preferredNodeId,
|
|
4895
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
4896
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
4897
|
+
);
|
|
3037
4898
|
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
3038
|
-
?
|
|
4899
|
+
? selectedCoordinatorNodeId
|
|
3039
4900
|
: undefined;
|
|
3040
4901
|
const refreshedAt = new Date().toISOString();
|
|
3041
4902
|
const nodeStatuses = [];
|
|
@@ -3050,11 +4911,15 @@ export class DaemonCommandRouter {
|
|
|
3050
4911
|
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
3051
4912
|
const status: Record<string, unknown> = {
|
|
3052
4913
|
nodeId,
|
|
3053
|
-
machineLabel: node
|
|
4914
|
+
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
4915
|
+
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
4916
|
+
? 'explicit_metadata'
|
|
4917
|
+
: 'workspace_host_provider_context',
|
|
3054
4918
|
workspace: node.workspace,
|
|
3055
4919
|
repoRoot: node.repoRoot,
|
|
3056
4920
|
isLocalWorktree: node.isLocalWorktree,
|
|
3057
4921
|
worktreeBranch: node.worktreeBranch,
|
|
4922
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
3058
4923
|
daemonId,
|
|
3059
4924
|
machineId: node.machineId,
|
|
3060
4925
|
machineStatus: node.machineStatus,
|
|
@@ -3095,8 +4960,20 @@ export class DaemonCommandRouter {
|
|
|
3095
4960
|
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
3096
4961
|
};
|
|
3097
4962
|
}
|
|
3098
|
-
const matchedLiveSessionRecords =
|
|
3099
|
-
|
|
4963
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
4964
|
+
meshId,
|
|
4965
|
+
node,
|
|
4966
|
+
nodeId,
|
|
4967
|
+
liveSessionRecords: liveMeshSessions,
|
|
4968
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4969
|
+
});
|
|
4970
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4971
|
+
meshId,
|
|
4972
|
+
nodeId,
|
|
4973
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
4974
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4975
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
4976
|
+
status.workspace = workspace || node.workspace;
|
|
3100
4977
|
if (matchedLiveSessionRecords.length > 0) {
|
|
3101
4978
|
const sessionIds = matchedLiveSessionRecords
|
|
3102
4979
|
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
@@ -3110,44 +4987,188 @@ export class DaemonCommandRouter {
|
|
|
3110
4987
|
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
3111
4988
|
}
|
|
3112
4989
|
}
|
|
3113
|
-
if (
|
|
3114
|
-
if (!fs.existsSync(
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
if (
|
|
4990
|
+
if (workspace) {
|
|
4991
|
+
if (!fs.existsSync(workspace)) {
|
|
4992
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
4993
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
4994
|
+
let remoteProbeApplied = false;
|
|
4995
|
+
if (inlineTransitGit) {
|
|
4996
|
+
status.git = inlineTransitGit;
|
|
4997
|
+
status.health = inlineTransitGit.isGitRepo
|
|
4998
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
4999
|
+
: 'degraded';
|
|
5000
|
+
const connection = readObjectRecord(status.connection);
|
|
5001
|
+
const connectionState = readStringValue(connection.state);
|
|
5002
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5003
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5004
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5005
|
+
}
|
|
5006
|
+
remoteProbeApplied = true;
|
|
5007
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
5008
|
+
try {
|
|
5009
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
5010
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5011
|
+
daemonId,
|
|
5012
|
+
workspace,
|
|
5013
|
+
timeoutMs: 8000,
|
|
5014
|
+
});
|
|
5015
|
+
if (remoteGit) {
|
|
5016
|
+
status.git = remoteGit;
|
|
5017
|
+
status.health = remoteGit.isGitRepo
|
|
5018
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
5019
|
+
: 'degraded';
|
|
5020
|
+
const connection = readObjectRecord(status.connection);
|
|
5021
|
+
const connectionState = readStringValue(connection.state);
|
|
5022
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5023
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5024
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5025
|
+
}
|
|
5026
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
5027
|
+
remoteProbeApplied = true;
|
|
5028
|
+
}
|
|
5029
|
+
} catch {
|
|
5030
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
5031
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
5032
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
5033
|
+
status.connection = refreshedConnection;
|
|
5034
|
+
try {
|
|
5035
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
5036
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5037
|
+
daemonId,
|
|
5038
|
+
workspace,
|
|
5039
|
+
timeoutMs: 12000,
|
|
5040
|
+
});
|
|
5041
|
+
if (remoteGit) {
|
|
5042
|
+
status.git = remoteGit;
|
|
5043
|
+
status.health = remoteGit.isGitRepo
|
|
5044
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
5045
|
+
: 'degraded';
|
|
5046
|
+
const connection = readObjectRecord(status.connection);
|
|
5047
|
+
const connectionState = readStringValue(connection.state);
|
|
5048
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5049
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5050
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5051
|
+
}
|
|
5052
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
5053
|
+
remoteProbeApplied = true;
|
|
5054
|
+
}
|
|
5055
|
+
} catch {
|
|
5056
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
3127
5060
|
}
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
5061
|
+
if (!remoteProbeApplied) {
|
|
5062
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
5063
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
5064
|
+
&& !isSelfNode
|
|
5065
|
+
&& !!daemonId
|
|
5066
|
+
&& (
|
|
5067
|
+
readStringValue(status.machineStatus) === 'online'
|
|
5068
|
+
|| readStringValue(status.health) === 'online'
|
|
5069
|
+
|| connectionState === 'connecting'
|
|
5070
|
+
|| connectionState === 'connected'
|
|
5071
|
+
|| connectionState === 'unknown'
|
|
5072
|
+
);
|
|
5073
|
+
if (pendingPeerGitProbe) {
|
|
5074
|
+
status.gitProbePending = true;
|
|
5075
|
+
status.health = 'unknown';
|
|
5076
|
+
}
|
|
5077
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
5078
|
+
status,
|
|
5079
|
+
node,
|
|
5080
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
5081
|
+
)) {
|
|
5082
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5083
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
5084
|
+
nodeStatuses.push(status);
|
|
5085
|
+
continue;
|
|
5086
|
+
}
|
|
5087
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
5088
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5089
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
5090
|
+
nodeStatuses.push(status);
|
|
5091
|
+
continue;
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
} else {
|
|
5095
|
+
try {
|
|
5096
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
5097
|
+
status.git = gitStatus;
|
|
5098
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
5099
|
+
if (gitStatus.isGitRepo) {
|
|
5100
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
5101
|
+
} else {
|
|
5102
|
+
status.health = 'degraded';
|
|
5103
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
5104
|
+
}
|
|
5105
|
+
} catch {
|
|
5106
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
5107
|
+
status.health = 'degraded';
|
|
5108
|
+
}
|
|
3131
5109
|
}
|
|
3132
5110
|
}
|
|
3133
5111
|
} else {
|
|
3134
5112
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
3135
5113
|
}
|
|
3136
|
-
|
|
5114
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5115
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
3137
5116
|
nodeStatuses.push(status);
|
|
3138
5117
|
}
|
|
3139
5118
|
|
|
3140
|
-
|
|
5119
|
+
const statusResult = {
|
|
3141
5120
|
success: true,
|
|
3142
5121
|
meshId: mesh.id,
|
|
3143
5122
|
meshName: mesh.name,
|
|
3144
5123
|
repoIdentity: mesh.repoIdentity,
|
|
3145
5124
|
defaultBranch: mesh.defaultBranch,
|
|
3146
|
-
refreshedAt
|
|
5125
|
+
refreshedAt,
|
|
5126
|
+
meshHost,
|
|
5127
|
+
sourceOfTruth: {
|
|
5128
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
5129
|
+
? 'coordinator_inline_mesh_cache'
|
|
5130
|
+
: meshRecord?.source === 'local_config'
|
|
5131
|
+
? 'local_mesh_config'
|
|
5132
|
+
: 'inline_bootstrap_snapshot',
|
|
5133
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
5134
|
+
meshHost: {
|
|
5135
|
+
owner: 'mesh_host_daemon',
|
|
5136
|
+
localRole: meshHost.role,
|
|
5137
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
5138
|
+
hostNodeId: meshHost.hostNodeId,
|
|
5139
|
+
hostAddress: meshHost.hostAddress,
|
|
5140
|
+
},
|
|
5141
|
+
...(requireDirectPeerTruth ? {
|
|
5142
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
5143
|
+
directPeerTruth: {
|
|
5144
|
+
required: true,
|
|
5145
|
+
satisfied: directTruthSatisfied,
|
|
5146
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
5147
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
5148
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
5149
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
5150
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
5151
|
+
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
5152
|
+
},
|
|
5153
|
+
} : {}),
|
|
5154
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
5155
|
+
},
|
|
5156
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
3147
5157
|
nodes: nodeStatuses,
|
|
3148
5158
|
queue: { tasks: queue, summary: queueSummary },
|
|
3149
5159
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
3150
5160
|
};
|
|
5161
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, statusResult, refreshReason);
|
|
5162
|
+
logRepoMeshStatusDebug('return_live', {
|
|
5163
|
+
meshId,
|
|
5164
|
+
command: 'mesh_status',
|
|
5165
|
+
refreshRequested,
|
|
5166
|
+
refreshReason,
|
|
5167
|
+
meshSource: meshRecord.source,
|
|
5168
|
+
directTruth,
|
|
5169
|
+
summary: summarizeRepoMeshStatusDebug(rememberedStatus),
|
|
5170
|
+
});
|
|
5171
|
+
return rememberedStatus;
|
|
3151
5172
|
} catch (e: any) {
|
|
3152
5173
|
return { success: false, error: e.message };
|
|
3153
5174
|
}
|
|
@@ -3205,7 +5226,7 @@ export class DaemonCommandRouter {
|
|
|
3205
5226
|
|
|
3206
5227
|
// 3. Kill OS process if requested
|
|
3207
5228
|
if (killProcess) {
|
|
3208
|
-
const running = isIdeRunning(ideType);
|
|
5229
|
+
const running = await isIdeRunning(ideType);
|
|
3209
5230
|
if (running) {
|
|
3210
5231
|
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
|
|
3211
5232
|
const killed = await killIdeProcess(ideType);
|