@adhdev/daemon-core 0.9.82-rc.7 → 0.9.82-rc.70
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/boot/daemon-lifecycle.d.ts +2 -0
- 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 +24 -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 +4619 -1143
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4582 -1128
- 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 +48 -0
- package/dist/mesh/mesh-events.d.ts +28 -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 +1 -0
- package/dist/repo-mesh-types.d.ts +160 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +4 -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/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +2452 -409
- 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 +39 -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 +4 -2
- package/src/mesh/mesh-active-work.ts +205 -0
- package/src/mesh/mesh-events.ts +291 -38
- 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 +66 -1
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +174 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
package/src/commands/router.ts
CHANGED
|
@@ -38,10 +38,22 @@ 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
59
|
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
@@ -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,11 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
|
137
230
|
return submodules.length > 0 ? submodules : undefined;
|
|
138
231
|
}
|
|
139
232
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
|
|
146
|
-
: [];
|
|
147
|
-
const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
|
|
148
|
-
const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
|
|
149
|
-
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
150
|
-
if (isGitRepo !== undefined) {
|
|
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
|
-
}
|
|
175
|
-
|
|
176
|
-
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
177
|
-
const gitResult = readObjectRecord(rawGit.result);
|
|
178
|
-
const directStatus = readObjectRecord(rawGit.status);
|
|
179
|
-
const nestedStatus = readObjectRecord(gitResult.status);
|
|
180
|
-
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
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
|
-
: {};
|
|
233
|
+
function normalizeInlineMeshGitStatus(
|
|
234
|
+
status: Record<string, unknown>,
|
|
235
|
+
node: any,
|
|
236
|
+
options?: { lastCheckedAt?: number },
|
|
237
|
+
): Record<string, unknown> | undefined {
|
|
194
238
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
239
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
240
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -198,15 +242,20 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
198
242
|
: [];
|
|
199
243
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
244
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
-
const
|
|
245
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
246
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
202
247
|
return {
|
|
203
248
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
-
repoRoot:
|
|
249
|
+
repoRoot: repoRoot ?? null,
|
|
205
250
|
isGitRepo,
|
|
206
251
|
branch: readStringValue(status.branch) ?? null,
|
|
207
252
|
headCommit: readStringValue(status.headCommit) ?? null,
|
|
208
253
|
headMessage: readStringValue(status.headMessage) ?? null,
|
|
209
254
|
upstream: readStringValue(status.upstream) ?? null,
|
|
255
|
+
upstreamStatus: readStringValue(status.upstreamStatus, status.upstream_status)
|
|
256
|
+
?? (readStringValue(status.upstream) ? 'unchecked' : 'no_upstream'),
|
|
257
|
+
upstreamFetchedAt: readNumberValue(status.upstreamFetchedAt, status.upstream_fetched_at),
|
|
258
|
+
upstreamFetchError: readStringValue(status.upstreamFetchError, status.upstream_fetch_error),
|
|
210
259
|
ahead: readNumberValue(status.ahead) ?? 0,
|
|
211
260
|
behind: readNumberValue(status.behind) ?? 0,
|
|
212
261
|
staged: readNumberValue(status.staged) ?? 0,
|
|
@@ -217,14 +266,247 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
217
266
|
hasConflicts,
|
|
218
267
|
conflictFiles,
|
|
219
268
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
-
lastCheckedAt: Date.now(),
|
|
269
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
221
270
|
...(submodules ? { submodules } : {}),
|
|
222
271
|
};
|
|
223
272
|
}
|
|
224
273
|
|
|
274
|
+
function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
|
|
275
|
+
if (!git) return Number.NEGATIVE_INFINITY;
|
|
276
|
+
let score = 0;
|
|
277
|
+
if (readBooleanValue(git.isGitRepo) === true) score += 50;
|
|
278
|
+
if (readBooleanValue(git.isGitRepo) === false) score -= 10;
|
|
279
|
+
if (readStringValue(git.branch)) score += 20;
|
|
280
|
+
if (readStringValue(git.headCommit)) score += 20;
|
|
281
|
+
if (readStringValue(git.upstream)) score += 10;
|
|
282
|
+
if (readStringValue(git.upstreamStatus)) score += 5;
|
|
283
|
+
if (readNumberValue(git.ahead) !== undefined) score += 2;
|
|
284
|
+
if (readNumberValue(git.behind) !== undefined) score += 2;
|
|
285
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
|
|
286
|
+
if (readStringValue(git.error)) score -= 20;
|
|
287
|
+
return score;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
291
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
292
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
293
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
294
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
295
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
296
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
297
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
298
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
299
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
300
|
+
const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
|
|
301
|
+
let best: { git: Record<string, unknown>; score: number } | null = null;
|
|
302
|
+
for (const status of candidates) {
|
|
303
|
+
const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
304
|
+
if (!normalized) continue;
|
|
305
|
+
const score = scoreInlineMeshGitStatus(normalized);
|
|
306
|
+
if (!best || score > best.score) best = { git: normalized, score };
|
|
307
|
+
}
|
|
308
|
+
return best?.git;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
|
|
312
|
+
if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
|
|
313
|
+
return snapshot.nodes.some((node: any) => {
|
|
314
|
+
if (node?.gitProbePending !== true) return false;
|
|
315
|
+
const git = readObjectRecord(node?.git);
|
|
316
|
+
return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
321
|
+
const source = readStringValue(connection.source);
|
|
322
|
+
const transport = readStringValue(connection.transport);
|
|
323
|
+
return {
|
|
324
|
+
...connection,
|
|
325
|
+
perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
|
|
326
|
+
source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
|
|
327
|
+
state: 'connected',
|
|
328
|
+
transport: transport && transport !== 'unknown' ? transport : 'direct',
|
|
329
|
+
reported: true,
|
|
330
|
+
reason: 'Live peer git snapshot reported by the selected coordinator.',
|
|
331
|
+
lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function recordInlineMeshDirectGitTruth(
|
|
336
|
+
node: any,
|
|
337
|
+
git: Record<string, unknown>,
|
|
338
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
339
|
+
): void {
|
|
340
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
341
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
342
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
343
|
+
const nextGit: Record<string, unknown> = {
|
|
344
|
+
...git,
|
|
345
|
+
lastCheckedAt: checkedAt,
|
|
346
|
+
};
|
|
347
|
+
node.lastGit = {
|
|
348
|
+
source,
|
|
349
|
+
checkedAt,
|
|
350
|
+
status: nextGit,
|
|
351
|
+
};
|
|
352
|
+
node.last_git = node.lastGit;
|
|
353
|
+
node.machineStatus = 'online';
|
|
354
|
+
node.updatedAt = updatedAt;
|
|
355
|
+
node.lastSeenAt = updatedAt;
|
|
356
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
357
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
361
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
362
|
+
if (liveGit) return liveGit;
|
|
363
|
+
|
|
364
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
365
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
366
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
367
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
371
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
372
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
373
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
374
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
375
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
376
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
377
|
+
const branch = readStringValue(cachedGit.branch);
|
|
378
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
379
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
383
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
384
|
+
const {
|
|
385
|
+
cachedStatus,
|
|
386
|
+
lastGit: _lastGit,
|
|
387
|
+
last_git: _lastGitLegacy,
|
|
388
|
+
lastProbe: _lastProbe,
|
|
389
|
+
last_probe: _lastProbeLegacy,
|
|
390
|
+
error: _error,
|
|
391
|
+
health: _health,
|
|
392
|
+
machineStatus: _machineStatus,
|
|
393
|
+
lastSeenAt: _lastSeenAt,
|
|
394
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
395
|
+
updatedAt: _updatedAt,
|
|
396
|
+
updated_at: _updatedAtLegacy,
|
|
397
|
+
activeSession: _activeSession,
|
|
398
|
+
active_session: _activeSessionLegacy,
|
|
399
|
+
activeSessionId: _activeSessionId,
|
|
400
|
+
active_session_id: _activeSessionIdLegacy,
|
|
401
|
+
sessionId: _sessionId,
|
|
402
|
+
session_id: _sessionIdLegacy,
|
|
403
|
+
providerType: _providerType,
|
|
404
|
+
provider_type: _providerTypeLegacy,
|
|
405
|
+
...rest
|
|
406
|
+
} = node as Record<string, unknown>;
|
|
407
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
408
|
+
return { ...rest, cachedStatus };
|
|
409
|
+
}
|
|
410
|
+
return rest;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
414
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
415
|
+
return 'cachedStatus' in node
|
|
416
|
+
|| 'lastGit' in node
|
|
417
|
+
|| 'last_git' in node
|
|
418
|
+
|| 'lastProbe' in node
|
|
419
|
+
|| 'last_probe' in node
|
|
420
|
+
|| 'error' in node
|
|
421
|
+
|| 'health' in node
|
|
422
|
+
|| 'machineStatus' in node
|
|
423
|
+
|| 'lastSeenAt' in node
|
|
424
|
+
|| 'last_seen_at' in node
|
|
425
|
+
|| 'updatedAt' in node
|
|
426
|
+
|| 'updated_at' in node
|
|
427
|
+
|| 'activeSession' in node
|
|
428
|
+
|| 'active_session' in node
|
|
429
|
+
|| 'activeSessionId' in node
|
|
430
|
+
|| 'active_session_id' in node
|
|
431
|
+
|| 'sessionId' in node
|
|
432
|
+
|| 'session_id' in node
|
|
433
|
+
|| 'providerType' in node
|
|
434
|
+
|| 'provider_type' in node;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
438
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
439
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
440
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function readInlineMeshNodeId(node: any): string {
|
|
444
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
448
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
449
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
450
|
+
let changed = false;
|
|
451
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
452
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
453
|
+
changed = true;
|
|
454
|
+
return stripInlineMeshTransientNodeState(node);
|
|
455
|
+
});
|
|
456
|
+
if (!changed) return inlineMesh;
|
|
457
|
+
return {
|
|
458
|
+
...inlineMesh,
|
|
459
|
+
nodes,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
464
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
465
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
466
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
467
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
468
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
469
|
+
|
|
470
|
+
const cachedUpdatedAt = Date.parse(readStringValue(cached.updatedAt, cached.updated_at) || '');
|
|
471
|
+
const incomingUpdatedAt = Date.parse(readStringValue(incoming.updatedAt, incoming.updated_at) || '');
|
|
472
|
+
const preserveCachedMembership = Number.isFinite(cachedUpdatedAt)
|
|
473
|
+
&& (!Number.isFinite(incomingUpdatedAt) || cachedUpdatedAt > incomingUpdatedAt);
|
|
474
|
+
|
|
475
|
+
const cachedById = new Map<string, any>();
|
|
476
|
+
for (const node of cachedNodes) {
|
|
477
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
478
|
+
if (nodeId) cachedById.set(nodeId, node);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const nodes = incomingNodes.map((incomingNode: any) => {
|
|
482
|
+
const nodeId = readInlineMeshNodeId(incomingNode);
|
|
483
|
+
const cachedNode = nodeId ? cachedById.get(nodeId) : undefined;
|
|
484
|
+
if (!cachedNode && preserveCachedMembership) return null;
|
|
485
|
+
if (!cachedNode) return incomingNode;
|
|
486
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
487
|
+
return { ...cachedNode, ...incomingNode };
|
|
488
|
+
}
|
|
489
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
490
|
+
}).filter(Boolean);
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
...cached,
|
|
494
|
+
...incoming,
|
|
495
|
+
nodes,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
225
499
|
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
226
|
-
|
|
227
|
-
|
|
500
|
+
return countGitWorktreeChanges(git) > 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function countGitWorktreeChanges(git: Record<string, unknown> | null | undefined): number {
|
|
504
|
+
if (!git) return 0;
|
|
505
|
+
return Number(git.staged || 0)
|
|
506
|
+
+ Number(git.modified || 0)
|
|
507
|
+
+ Number(git.untracked || 0)
|
|
508
|
+
+ Number(git.deleted || 0)
|
|
509
|
+
+ Number(git.renamed || 0);
|
|
228
510
|
}
|
|
229
511
|
|
|
230
512
|
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
@@ -249,6 +531,167 @@ function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undef
|
|
|
249
531
|
return 'online';
|
|
250
532
|
}
|
|
251
533
|
|
|
534
|
+
function readMeshNodeLabel(status: Record<string, unknown>, node: any): string {
|
|
535
|
+
return readStringValue(status.nodeId, node?.id, node?.nodeId) ?? 'unknown';
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function buildInlineMeshBranchConvergence(args: {
|
|
539
|
+
mesh: any;
|
|
540
|
+
node: any;
|
|
541
|
+
status: Record<string, unknown>;
|
|
542
|
+
}): Record<string, unknown> {
|
|
543
|
+
const git = readObjectRecord(args.status.git);
|
|
544
|
+
const nodeLabel = readMeshNodeLabel(args.status, args.node);
|
|
545
|
+
const defaultBranch = readStringValue(args.mesh?.defaultBranch) ?? 'main';
|
|
546
|
+
const branch = readStringValue(git.branch, args.node?.worktreeBranch) ?? null;
|
|
547
|
+
const upstream = readStringValue(git.upstream) ?? null;
|
|
548
|
+
const upstreamStatus = readStringValue(git.upstreamStatus, git.upstream_status)
|
|
549
|
+
?? (upstream ? 'unchecked' : 'no_upstream');
|
|
550
|
+
const ahead = readNumberValue(git.ahead) ?? 0;
|
|
551
|
+
const behind = readNumberValue(git.behind) ?? 0;
|
|
552
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
553
|
+
const hasConflicts = readBooleanValue(git.hasConflicts)
|
|
554
|
+
?? (Array.isArray(git.conflictFiles) && git.conflictFiles.length > 0);
|
|
555
|
+
const base = {
|
|
556
|
+
defaultBranch,
|
|
557
|
+
branch,
|
|
558
|
+
upstream,
|
|
559
|
+
upstreamStatus,
|
|
560
|
+
ahead,
|
|
561
|
+
behind,
|
|
562
|
+
isWorktree: args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true,
|
|
563
|
+
isDefaultBranch: branch === defaultBranch,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
if (readBooleanValue(git.isGitRepo) !== true) {
|
|
567
|
+
return {
|
|
568
|
+
...base,
|
|
569
|
+
status: 'blocked_review',
|
|
570
|
+
needsConvergence: true,
|
|
571
|
+
reason: 'git_status_unavailable',
|
|
572
|
+
nextStep: `Resolve git status for node '${nodeLabel}' before marking the task complete.`,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!branch) {
|
|
577
|
+
return {
|
|
578
|
+
...base,
|
|
579
|
+
status: 'blocked_review',
|
|
580
|
+
needsConvergence: true,
|
|
581
|
+
reason: 'branch_unknown',
|
|
582
|
+
nextStep: `Inspect node '${nodeLabel}' git branch before deciding whether it is merged to ${defaultBranch}.`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (hasConflicts || uncommittedChanges > 0) {
|
|
587
|
+
return {
|
|
588
|
+
...base,
|
|
589
|
+
status: 'not_mergeable',
|
|
590
|
+
needsConvergence: true,
|
|
591
|
+
reason: hasConflicts ? 'conflicts_present' : 'dirty_workspace',
|
|
592
|
+
nextStep: `Commit, checkpoint, or resolve node '${nodeLabel}' before any main convergence step.`,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (branch === defaultBranch) {
|
|
597
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
598
|
+
return {
|
|
599
|
+
...base,
|
|
600
|
+
status: 'blocked_review',
|
|
601
|
+
needsConvergence: true,
|
|
602
|
+
reason: 'default_branch_upstream_unverified',
|
|
603
|
+
nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${nodeLabel}'.`,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
if (ahead > 0 || behind > 0) {
|
|
607
|
+
return {
|
|
608
|
+
...base,
|
|
609
|
+
status: 'blocked_review',
|
|
610
|
+
needsConvergence: true,
|
|
611
|
+
reason: 'default_branch_not_even_with_upstream',
|
|
612
|
+
nextStep: `Bring ${defaultBranch} even with its upstream before declaring convergence complete.`,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
...base,
|
|
617
|
+
status: 'merged_to_main',
|
|
618
|
+
needsConvergence: false,
|
|
619
|
+
reason: 'clean_default_branch',
|
|
620
|
+
nextStep: null,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true) {
|
|
625
|
+
return {
|
|
626
|
+
...base,
|
|
627
|
+
status: 'cleanup_candidate',
|
|
628
|
+
needsConvergence: true,
|
|
629
|
+
reason: 'clean_non_default_worktree_branch',
|
|
630
|
+
nextStep: `Run mesh_refine_node(node_id: "${nodeLabel}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
635
|
+
return {
|
|
636
|
+
...base,
|
|
637
|
+
status: 'blocked_review',
|
|
638
|
+
needsConvergence: true,
|
|
639
|
+
reason: 'feature_branch_upstream_unverified',
|
|
640
|
+
nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!upstream || ahead > 0 || behind > 0) {
|
|
645
|
+
return {
|
|
646
|
+
...base,
|
|
647
|
+
status: 'blocked_review',
|
|
648
|
+
needsConvergence: true,
|
|
649
|
+
reason: !upstream ? 'feature_branch_missing_upstream' : 'feature_branch_not_even_with_upstream',
|
|
650
|
+
nextStep: `Push or reconcile branch '${branch}', then merge it into ${defaultBranch} or mark it not_mergeable with a reason.`,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
...base,
|
|
656
|
+
status: 'pushed_feature_branch_needs_merge',
|
|
657
|
+
needsConvergence: true,
|
|
658
|
+
reason: 'clean_non_default_branch',
|
|
659
|
+
nextStep: `Review and merge branch '${branch}' into ${defaultBranch}; do not report the task as fully complete while it remains off main.`,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
664
|
+
const git = readObjectRecord(status.git);
|
|
665
|
+
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
666
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
667
|
+
status.isDirty = uncommittedChanges > 0;
|
|
668
|
+
status.uncommittedChanges = uncommittedChanges;
|
|
669
|
+
status.branchConvergence = buildInlineMeshBranchConvergence({ mesh, node, status });
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
673
|
+
const followUps = nodes
|
|
674
|
+
.filter(node => readObjectRecord(node.branchConvergence).needsConvergence === true)
|
|
675
|
+
.map(node => {
|
|
676
|
+
const convergence = readObjectRecord(node.branchConvergence);
|
|
677
|
+
return {
|
|
678
|
+
nodeId: node.nodeId,
|
|
679
|
+
workspace: node.workspace,
|
|
680
|
+
branch: convergence.branch,
|
|
681
|
+
status: convergence.status,
|
|
682
|
+
reason: convergence.reason,
|
|
683
|
+
nextStep: convergence.nextStep,
|
|
684
|
+
};
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
needsFollowUp: followUps.length > 0,
|
|
689
|
+
unresolvedCount: followUps.length,
|
|
690
|
+
requiredFinalStates: ['merged_to_main', 'pushed_feature_branch_needs_merge', 'blocked_review', 'cleanup_candidate', 'not_mergeable'],
|
|
691
|
+
followUps,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
252
695
|
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
253
696
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
254
697
|
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
@@ -259,17 +702,313 @@ function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
|
259
702
|
return sessionId ? [sessionId] : [];
|
|
260
703
|
}
|
|
261
704
|
|
|
262
|
-
function
|
|
705
|
+
function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
706
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
707
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
708
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
709
|
+
? activeSession
|
|
710
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
711
|
+
const sessionId = readStringValue(
|
|
712
|
+
fallbackSession.id,
|
|
713
|
+
fallbackSession.sessionId,
|
|
714
|
+
fallbackSession.session_id,
|
|
715
|
+
node?.activeSessionId,
|
|
716
|
+
node?.active_session_id,
|
|
717
|
+
node?.sessionId,
|
|
718
|
+
node?.session_id,
|
|
719
|
+
);
|
|
720
|
+
if (!sessionId) return [];
|
|
721
|
+
return [{
|
|
722
|
+
sessionId,
|
|
723
|
+
providerType: readStringValue(
|
|
724
|
+
fallbackSession.providerType,
|
|
725
|
+
fallbackSession.provider_type,
|
|
726
|
+
fallbackSession.cliType,
|
|
727
|
+
fallbackSession.cli_type,
|
|
728
|
+
fallbackSession.provider,
|
|
729
|
+
node?.providerType,
|
|
730
|
+
node?.provider_type,
|
|
731
|
+
),
|
|
732
|
+
state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
|
|
733
|
+
lifecycle: readStringValue(fallbackSession.lifecycle),
|
|
734
|
+
title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
|
|
735
|
+
workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
|
|
736
|
+
lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
|
|
737
|
+
recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
|
|
738
|
+
isCached: true,
|
|
739
|
+
}];
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function readLiveMeshSessionState(record: any): string | undefined {
|
|
743
|
+
return readStringValue(
|
|
744
|
+
record?.meta?.sessionStatus,
|
|
745
|
+
record?.meta?.status,
|
|
746
|
+
record?.meta?.providerStatus,
|
|
747
|
+
record?.status,
|
|
748
|
+
record?.state,
|
|
749
|
+
record?.lifecycle,
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function toIsoTimestamp(value: unknown): string | null {
|
|
754
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
755
|
+
const stringValue = readStringValue(value);
|
|
756
|
+
return stringValue || null;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
760
|
+
const connection = readObjectRecord(status.connection);
|
|
761
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
762
|
+
const git = readObjectRecord(status.git);
|
|
763
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
764
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
765
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
766
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function finalizeMeshNodeStatus(args: {
|
|
771
|
+
status: Record<string, unknown>;
|
|
772
|
+
node: any;
|
|
773
|
+
daemonId?: string;
|
|
774
|
+
isSelfNode: boolean;
|
|
775
|
+
}): void {
|
|
776
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
777
|
+
if (!readStringValue(status.machineStatus)) {
|
|
778
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
779
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
780
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
781
|
+
}
|
|
782
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
783
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
784
|
+
status.launchReady = !!daemonId && (
|
|
785
|
+
readStringValue(status.machineStatus) === 'online'
|
|
786
|
+
|| connectionState === 'connected'
|
|
787
|
+
|| isSelfNode
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
792
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
793
|
+
daemonId: string;
|
|
794
|
+
workspace: string;
|
|
795
|
+
timeoutMs: number;
|
|
796
|
+
}): Promise<Record<string, unknown> | null> {
|
|
797
|
+
if (!args.dispatchMeshCommand) return null;
|
|
798
|
+
const remoteResult = await Promise.race([
|
|
799
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
|
|
800
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
801
|
+
]) as any;
|
|
802
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
803
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
804
|
+
? remoteGit as Record<string, unknown>
|
|
805
|
+
: null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
809
|
+
mesh: any;
|
|
810
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
811
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
812
|
+
statusInstanceId?: string;
|
|
813
|
+
localMachineId?: string;
|
|
814
|
+
}): Promise<{
|
|
815
|
+
directEvidenceCount: number;
|
|
816
|
+
localConfirmedCount: number;
|
|
817
|
+
peerAttemptedCount: number;
|
|
818
|
+
peerConfirmedCount: number;
|
|
819
|
+
unavailableNodeIds: string[];
|
|
820
|
+
}> {
|
|
821
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
822
|
+
if (!nodes.length) {
|
|
823
|
+
return {
|
|
824
|
+
directEvidenceCount: 0,
|
|
825
|
+
localConfirmedCount: 0,
|
|
826
|
+
peerAttemptedCount: 0,
|
|
827
|
+
peerConfirmedCount: 0,
|
|
828
|
+
unavailableNodeIds: [],
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
833
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
834
|
+
nodes[0]?.id,
|
|
835
|
+
nodes[0]?.nodeId,
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
let localConfirmedCount = 0;
|
|
839
|
+
let peerAttemptedCount = 0;
|
|
840
|
+
let peerConfirmedCount = 0;
|
|
841
|
+
const unavailableNodeIds: string[] = [];
|
|
842
|
+
|
|
843
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
844
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
845
|
+
const workspace = readStringValue(node?.workspace);
|
|
846
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
847
|
+
const isSelfNode = Boolean(
|
|
848
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
849
|
+
) || Boolean(
|
|
850
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
851
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
852
|
+
|
|
853
|
+
if (!workspace) {
|
|
854
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (fs.existsSync(workspace)) {
|
|
859
|
+
try {
|
|
860
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
861
|
+
if (localGit?.isGitRepo) {
|
|
862
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
863
|
+
localConfirmedCount += 1;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
} catch {
|
|
867
|
+
// Fall through to remote classification.
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
872
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
peerAttemptedCount += 1;
|
|
877
|
+
try {
|
|
878
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
879
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
880
|
+
daemonId,
|
|
881
|
+
workspace,
|
|
882
|
+
timeoutMs: 8_000,
|
|
883
|
+
});
|
|
884
|
+
if (remoteGit) {
|
|
885
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
886
|
+
peerConfirmedCount += 1;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
} catch {
|
|
890
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
unavailableNodeIds.push(nodeId);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
898
|
+
localConfirmedCount,
|
|
899
|
+
peerAttemptedCount,
|
|
900
|
+
peerConfirmedCount,
|
|
901
|
+
unavailableNodeIds,
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
906
|
+
return {
|
|
907
|
+
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
908
|
+
providerType: readStringValue(record?.providerType),
|
|
909
|
+
state: readLiveMeshSessionState(record),
|
|
910
|
+
lifecycle: readStringValue(record?.lifecycle),
|
|
911
|
+
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
912
|
+
recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
|
|
913
|
+
workspace: readStringValue(record?.workspace) ?? null,
|
|
914
|
+
title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
|
|
915
|
+
lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
|
|
916
|
+
isCached: false,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
921
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
922
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
923
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
924
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
928
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
929
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
930
|
+
|
|
931
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
932
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
933
|
+
|
|
934
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
938
|
+
meshId: string;
|
|
939
|
+
nodeId: string;
|
|
940
|
+
liveSessionRecords: any[];
|
|
941
|
+
allowCoordinatorSession?: boolean;
|
|
942
|
+
}): string {
|
|
943
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
944
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
945
|
+
&& readStringValue(record?.workspace)
|
|
946
|
+
));
|
|
947
|
+
if (directNodeWorkspace) {
|
|
948
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (args.allowCoordinatorSession) {
|
|
952
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
953
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
954
|
+
&& readStringValue(record?.workspace)
|
|
955
|
+
));
|
|
956
|
+
if (coordinatorWorkspace) {
|
|
957
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return '';
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function collectLiveMeshSessionRecords(args: {
|
|
965
|
+
meshId: string;
|
|
966
|
+
node: any;
|
|
967
|
+
nodeId: string;
|
|
968
|
+
liveSessionRecords: any[];
|
|
969
|
+
allowCoordinatorSession?: boolean;
|
|
970
|
+
}): any[] {
|
|
971
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
972
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
973
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
|
|
974
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
if (args.allowCoordinatorSession) {
|
|
978
|
+
for (const record of args.liveSessionRecords) {
|
|
979
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
980
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
981
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
982
|
+
matches.push(record);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return matches;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function applyCachedInlineMeshNodeStatus(
|
|
990
|
+
status: Record<string, unknown>,
|
|
991
|
+
node: any,
|
|
992
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
993
|
+
): boolean {
|
|
263
994
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const
|
|
995
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
996
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
997
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
998
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
267
999
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
1000
|
+
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
1001
|
+
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
268
1002
|
const activeSessions = readCachedInlineMeshActiveSessions(node);
|
|
269
|
-
|
|
1003
|
+
const activeSessionDetails = readCachedInlineMeshActiveSessionDetails(node);
|
|
1004
|
+
if (!git && !error && !health && !machineStatus && !lastSeenAt && !updatedAt && activeSessions.length === 0) return false;
|
|
270
1005
|
if (git) status.git = git;
|
|
271
1006
|
if (error) status.error = error;
|
|
1007
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
1008
|
+
if (lastSeenAt) status.lastSeenAt = lastSeenAt;
|
|
1009
|
+
if (updatedAt) status.updatedAt = updatedAt;
|
|
272
1010
|
if (activeSessions.length > 0) status.activeSessions = activeSessions;
|
|
1011
|
+
if (activeSessionDetails.length > 0) status.activeSessionDetails = activeSessionDetails;
|
|
273
1012
|
if (health) {
|
|
274
1013
|
status.health = health;
|
|
275
1014
|
return true;
|
|
@@ -278,7 +1017,7 @@ function applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node:
|
|
|
278
1017
|
status.health = deriveMeshNodeHealthFromGit(git);
|
|
279
1018
|
return true;
|
|
280
1019
|
}
|
|
281
|
-
return activeSessions.length > 0 || !!machineStatus;
|
|
1020
|
+
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
282
1021
|
}
|
|
283
1022
|
|
|
284
1023
|
async function resolveProviderTypeFromPriority(args: {
|
|
@@ -313,13 +1052,7 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
313
1052
|
}
|
|
314
1053
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
315
1054
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
316
|
-
type MeshRefineValidationCommand =
|
|
317
|
-
command: string;
|
|
318
|
-
args: string[];
|
|
319
|
-
displayCommand: string;
|
|
320
|
-
category: string;
|
|
321
|
-
source: string;
|
|
322
|
-
};
|
|
1055
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
323
1056
|
|
|
324
1057
|
type MeshRefineValidationSummary = {
|
|
325
1058
|
status: MeshRefineValidationStatus;
|
|
@@ -329,13 +1062,83 @@ type MeshRefineValidationSummary = {
|
|
|
329
1062
|
skippedReason?: string;
|
|
330
1063
|
timeoutMs: number;
|
|
331
1064
|
outputLimitBytes: number;
|
|
1065
|
+
configSource?: string;
|
|
1066
|
+
configSourceType?: string;
|
|
1067
|
+
suggestions?: unknown[];
|
|
1068
|
+
suggestedConfig?: unknown;
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
1072
|
+
|
|
1073
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
1074
|
+
status: MeshRefineStageStatus;
|
|
1075
|
+
equivalent: boolean;
|
|
1076
|
+
baseHead: string;
|
|
1077
|
+
branchHead: string;
|
|
1078
|
+
mergeBase?: string;
|
|
1079
|
+
mergedTree?: string;
|
|
1080
|
+
expectedPatchId?: string;
|
|
1081
|
+
actualPatchId?: string;
|
|
1082
|
+
durationMs: number;
|
|
1083
|
+
error?: string;
|
|
1084
|
+
stdout?: string;
|
|
1085
|
+
stderr?: string;
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
type MeshRefineSubmoduleReachabilityEntry = {
|
|
1089
|
+
path: string;
|
|
1090
|
+
commit: string;
|
|
1091
|
+
reachable: boolean;
|
|
1092
|
+
checkedLocal?: boolean;
|
|
1093
|
+
fetchedFromOrigin?: boolean;
|
|
1094
|
+
error?: string;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
type MeshRefineSubmoduleReachabilitySummary = {
|
|
1098
|
+
status: MeshRefineStageStatus;
|
|
1099
|
+
checked: number;
|
|
1100
|
+
unreachable: MeshRefineSubmoduleReachabilityEntry[];
|
|
1101
|
+
entries: MeshRefineSubmoduleReachabilityEntry[];
|
|
1102
|
+
durationMs: number;
|
|
1103
|
+
error?: string;
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1107
|
+
|
|
1108
|
+
type MeshRefineJobHandle = {
|
|
1109
|
+
success: true;
|
|
1110
|
+
async: true;
|
|
1111
|
+
status: MeshRefineAsyncJobStatus;
|
|
1112
|
+
jobId: string;
|
|
1113
|
+
interactionId: string;
|
|
1114
|
+
meshId: string;
|
|
1115
|
+
nodeId: string;
|
|
1116
|
+
targetNodeId: string;
|
|
1117
|
+
targetDaemonId?: string;
|
|
1118
|
+
workspace?: string;
|
|
1119
|
+
startedAt: string;
|
|
1120
|
+
completedAt?: string;
|
|
1121
|
+
duplicate?: boolean;
|
|
1122
|
+
retryOfJobId?: string;
|
|
1123
|
+
eventDelivery: {
|
|
1124
|
+
pendingEvents: true;
|
|
1125
|
+
ledger: true;
|
|
1126
|
+
};
|
|
1127
|
+
evidence: {
|
|
1128
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
1129
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
1130
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
1131
|
+
};
|
|
332
1132
|
};
|
|
333
1133
|
|
|
1134
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
1135
|
+
|
|
334
1136
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
335
1137
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
336
1138
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
337
1139
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
338
1140
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
1141
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
339
1142
|
|
|
340
1143
|
function truncateValidationOutput(value: unknown): string {
|
|
341
1144
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -343,171 +1146,203 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
343
1146
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
344
1147
|
}
|
|
345
1148
|
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const trimmed = command.trim();
|
|
360
|
-
if (!trimmed) return null;
|
|
361
|
-
// Fail closed: the gate never hands shell syntax to a shell. Package-manager
|
|
362
|
-
// scripts are invoked via execFile(binary, args), and metacharacters/quotes are
|
|
363
|
-
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
364
|
-
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
365
|
-
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
366
|
-
if (!tokens.length) return null;
|
|
367
|
-
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
368
|
-
return tokens;
|
|
1149
|
+
function recordMeshRefineStage(
|
|
1150
|
+
stages: Array<Record<string, unknown>>,
|
|
1151
|
+
stage: string,
|
|
1152
|
+
status: MeshRefineStageStatus,
|
|
1153
|
+
startedAt: number,
|
|
1154
|
+
details?: Record<string, unknown>,
|
|
1155
|
+
): void {
|
|
1156
|
+
stages.push({
|
|
1157
|
+
stage,
|
|
1158
|
+
status,
|
|
1159
|
+
durationMs: Date.now() - startedAt,
|
|
1160
|
+
...(details || {}),
|
|
1161
|
+
});
|
|
369
1162
|
}
|
|
370
1163
|
|
|
371
|
-
function
|
|
372
|
-
|
|
1164
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
1165
|
+
const { execFileSync } = await import('node:child_process');
|
|
1166
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
1167
|
+
cwd,
|
|
1168
|
+
encoding: 'utf8',
|
|
1169
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1170
|
+
});
|
|
1171
|
+
if (!diff.trim()) return '';
|
|
1172
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
1173
|
+
cwd,
|
|
1174
|
+
input: diff,
|
|
1175
|
+
encoding: 'utf8',
|
|
1176
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1177
|
+
}).trim();
|
|
1178
|
+
return patchId.split(/\s+/)[0] || '';
|
|
373
1179
|
}
|
|
374
1180
|
|
|
375
|
-
function
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
1181
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
1182
|
+
repoRoot: string,
|
|
1183
|
+
baseHead: string,
|
|
1184
|
+
branchHead: string,
|
|
1185
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
1186
|
+
const startedAt = Date.now();
|
|
1187
|
+
try {
|
|
1188
|
+
const { execFileSync } = await import('node:child_process');
|
|
1189
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
1190
|
+
cwd: repoRoot,
|
|
1191
|
+
encoding: 'utf8',
|
|
1192
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1193
|
+
});
|
|
1194
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
1195
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
1196
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
1197
|
+
if (!mergeBase || !mergedTree) {
|
|
1198
|
+
return {
|
|
1199
|
+
status: 'failed',
|
|
1200
|
+
equivalent: false,
|
|
1201
|
+
baseHead,
|
|
1202
|
+
branchHead,
|
|
1203
|
+
mergeBase: mergeBase || undefined,
|
|
1204
|
+
mergedTree: mergedTree || undefined,
|
|
1205
|
+
durationMs: Date.now() - startedAt,
|
|
1206
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
1207
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
1211
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
1212
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
1213
|
+
return {
|
|
1214
|
+
status: equivalent ? 'passed' : 'failed',
|
|
1215
|
+
equivalent,
|
|
1216
|
+
baseHead,
|
|
1217
|
+
branchHead,
|
|
1218
|
+
mergeBase,
|
|
1219
|
+
mergedTree,
|
|
1220
|
+
expectedPatchId,
|
|
1221
|
+
actualPatchId,
|
|
1222
|
+
durationMs: Date.now() - startedAt,
|
|
1223
|
+
};
|
|
1224
|
+
} catch (e: any) {
|
|
1225
|
+
return {
|
|
1226
|
+
status: 'failed',
|
|
1227
|
+
equivalent: false,
|
|
1228
|
+
baseHead,
|
|
1229
|
+
branchHead,
|
|
1230
|
+
durationMs: Date.now() - startedAt,
|
|
1231
|
+
error: e?.message || String(e),
|
|
1232
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1233
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1234
|
+
};
|
|
412
1235
|
}
|
|
413
|
-
|
|
414
|
-
return {
|
|
415
|
-
command: {
|
|
416
|
-
command,
|
|
417
|
-
args,
|
|
418
|
-
displayCommand: [command, ...args].join(' '),
|
|
419
|
-
category,
|
|
420
|
-
source,
|
|
421
|
-
},
|
|
422
|
-
};
|
|
423
1236
|
}
|
|
424
1237
|
|
|
425
|
-
function
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1238
|
+
async function runMeshRefineSubmoduleReachabilityGate(
|
|
1239
|
+
repoRoot: string,
|
|
1240
|
+
mergedTree: string,
|
|
1241
|
+
): Promise<MeshRefineSubmoduleReachabilitySummary> {
|
|
1242
|
+
const startedAt = Date.now();
|
|
1243
|
+
const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
|
|
1244
|
+
try {
|
|
1245
|
+
const { execFile } = await import('node:child_process');
|
|
1246
|
+
const { promisify } = await import('node:util');
|
|
1247
|
+
const execFileAsync = promisify(execFile);
|
|
1248
|
+
const runGit = async (cwd: string, args: string[]): Promise<string> => {
|
|
1249
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
1250
|
+
cwd,
|
|
1251
|
+
encoding: 'utf8',
|
|
1252
|
+
timeout: 30_000,
|
|
1253
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1254
|
+
windowsHide: true,
|
|
438
1255
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return candidates.sort((a, b) => {
|
|
442
|
-
const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
|
|
443
|
-
return rank(a.confidence) - rank(b.confidence);
|
|
444
|
-
});
|
|
445
|
-
}
|
|
1256
|
+
return String(stdout || '');
|
|
1257
|
+
};
|
|
446
1258
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
1259
|
+
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1260
|
+
const gitlinks = treeOutput
|
|
1261
|
+
.split('\0')
|
|
1262
|
+
.filter(Boolean)
|
|
1263
|
+
.map(record => {
|
|
1264
|
+
const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
1265
|
+
return match ? { commit: match[1], path: match[2] } : null;
|
|
1266
|
+
})
|
|
1267
|
+
.filter((entry): entry is { commit: string; path: string } => !!entry);
|
|
1268
|
+
|
|
1269
|
+
for (const gitlink of gitlinks) {
|
|
1270
|
+
const submodulePath = pathResolve(repoRoot, gitlink.path);
|
|
1271
|
+
const entry: MeshRefineSubmoduleReachabilityEntry = {
|
|
1272
|
+
path: gitlink.path,
|
|
1273
|
+
commit: gitlink.commit,
|
|
1274
|
+
reachable: false,
|
|
1275
|
+
};
|
|
1276
|
+
try {
|
|
1277
|
+
if (!fs.existsSync(submodulePath)) {
|
|
1278
|
+
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1279
|
+
entries.push(entry);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
464
1282
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (!selected.length && candidates.length === 0) {
|
|
488
|
-
for (const category of REFINE_VALIDATION_CATEGORIES) {
|
|
489
|
-
if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
|
|
490
|
-
const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, 'package.json:scripts');
|
|
491
|
-
if (fallback.command && !seen.has(fallback.command.displayCommand)) {
|
|
492
|
-
selected.push(fallback.command);
|
|
493
|
-
seen.add(fallback.command.displayCommand);
|
|
494
|
-
} else if (fallback.rejected) {
|
|
495
|
-
rejectedCommands.push(fallback.rejected);
|
|
496
|
-
}
|
|
497
|
-
if (selected.length >= 2) break;
|
|
1283
|
+
entry.checkedLocal = true;
|
|
1284
|
+
try {
|
|
1285
|
+
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1286
|
+
entry.reachable = true;
|
|
1287
|
+
entries.push(entry);
|
|
1288
|
+
continue;
|
|
1289
|
+
} catch {
|
|
1290
|
+
// Probe the submodule remote before allowing cleanup/completion.
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
try {
|
|
1294
|
+
await runGit(submodulePath, ['fetch', 'origin', gitlink.commit]);
|
|
1295
|
+
entry.fetchedFromOrigin = true;
|
|
1296
|
+
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1297
|
+
entry.reachable = true;
|
|
1298
|
+
} catch (e: any) {
|
|
1299
|
+
entry.error = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1300
|
+
}
|
|
1301
|
+
} catch (e: any) {
|
|
1302
|
+
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
1303
|
+
}
|
|
1304
|
+
entries.push(entry);
|
|
498
1305
|
}
|
|
1306
|
+
|
|
1307
|
+
const unreachable = entries.filter(entry => !entry.reachable);
|
|
1308
|
+
return {
|
|
1309
|
+
status: unreachable.length ? 'failed' : 'passed',
|
|
1310
|
+
checked: entries.length,
|
|
1311
|
+
unreachable,
|
|
1312
|
+
entries,
|
|
1313
|
+
durationMs: Date.now() - startedAt,
|
|
1314
|
+
};
|
|
1315
|
+
} catch (e: any) {
|
|
1316
|
+
return {
|
|
1317
|
+
status: 'failed',
|
|
1318
|
+
checked: entries.length,
|
|
1319
|
+
unreachable: entries.filter(entry => !entry.reachable),
|
|
1320
|
+
entries,
|
|
1321
|
+
durationMs: Date.now() - startedAt,
|
|
1322
|
+
error: truncateValidationOutput(e?.message || String(e)),
|
|
1323
|
+
};
|
|
499
1324
|
}
|
|
1325
|
+
}
|
|
500
1326
|
|
|
1327
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1328
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
501
1329
|
return {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1330
|
+
source: plan.source,
|
|
1331
|
+
sourceType: plan.sourceType,
|
|
1332
|
+
commands: plan.commands.map(command => ({
|
|
1333
|
+
displayCommand: command.displayCommand,
|
|
1334
|
+
category: command.category,
|
|
1335
|
+
source: command.source,
|
|
1336
|
+
cwd: command.cwd,
|
|
1337
|
+
timeoutMs: command.timeoutMs,
|
|
1338
|
+
})),
|
|
1339
|
+
unavailableReason: plan.unavailableReason,
|
|
1340
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1341
|
+
suggestions: plan.suggestions,
|
|
1342
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1343
|
+
note: plan.sourceType === 'unavailable'
|
|
1344
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1345
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
511
1346
|
};
|
|
512
1347
|
}
|
|
513
1348
|
|
|
@@ -515,7 +1350,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
515
1350
|
const { execFile } = await import('node:child_process');
|
|
516
1351
|
const { promisify } = await import('node:util');
|
|
517
1352
|
const execFileAsync = promisify(execFile);
|
|
518
|
-
const selection =
|
|
1353
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
519
1354
|
const summary: MeshRefineValidationSummary = {
|
|
520
1355
|
status: 'skipped',
|
|
521
1356
|
required: true,
|
|
@@ -524,22 +1359,28 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
524
1359
|
skippedReason: undefined,
|
|
525
1360
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
526
1361
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1362
|
+
configSource: selection.source,
|
|
1363
|
+
configSourceType: selection.sourceType,
|
|
1364
|
+
suggestions: selection.suggestions,
|
|
1365
|
+
suggestedConfig: selection.suggestedConfig,
|
|
527
1366
|
};
|
|
528
1367
|
|
|
529
1368
|
if (!selection.commands.length) {
|
|
530
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1369
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
531
1370
|
return summary;
|
|
532
1371
|
}
|
|
533
1372
|
|
|
534
1373
|
for (const candidate of selection.commands) {
|
|
535
1374
|
const startedAt = Date.now();
|
|
1375
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1376
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
536
1377
|
try {
|
|
537
1378
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
538
|
-
cwd
|
|
1379
|
+
cwd,
|
|
539
1380
|
encoding: 'utf8',
|
|
540
|
-
timeout
|
|
1381
|
+
timeout,
|
|
541
1382
|
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
542
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
1383
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
543
1384
|
});
|
|
544
1385
|
summary.commandsRun.push({
|
|
545
1386
|
command: candidate.command,
|
|
@@ -547,6 +1388,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
547
1388
|
displayCommand: candidate.displayCommand,
|
|
548
1389
|
category: candidate.category,
|
|
549
1390
|
source: candidate.source,
|
|
1391
|
+
cwd,
|
|
550
1392
|
passed: true,
|
|
551
1393
|
exitCode: 0,
|
|
552
1394
|
durationMs: Date.now() - startedAt,
|
|
@@ -560,6 +1402,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
560
1402
|
displayCommand: candidate.displayCommand,
|
|
561
1403
|
category: candidate.category,
|
|
562
1404
|
source: candidate.source,
|
|
1405
|
+
cwd,
|
|
563
1406
|
passed: false,
|
|
564
1407
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
565
1408
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
@@ -698,6 +1541,10 @@ export interface CommandRouterDeps {
|
|
|
698
1541
|
statusVersion?: string;
|
|
699
1542
|
/** Session host control plane */
|
|
700
1543
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
1544
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
1545
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1546
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
1547
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
701
1548
|
}
|
|
702
1549
|
|
|
703
1550
|
export interface CommandRouterResult {
|
|
@@ -809,42 +1656,262 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
809
1656
|
};
|
|
810
1657
|
}
|
|
811
1658
|
|
|
1659
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
1660
|
+
const raw = hostAddress.trim();
|
|
1661
|
+
if (!raw) throw new Error('hostAddress required');
|
|
1662
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
1663
|
+
url.pathname = '/api/v1/command';
|
|
1664
|
+
url.search = '';
|
|
1665
|
+
url.hash = '';
|
|
1666
|
+
return url.toString();
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
1670
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
1671
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
1672
|
+
? args.memberNode as Record<string, any>
|
|
1673
|
+
: null;
|
|
1674
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
1675
|
+
? (requestedNodeId
|
|
1676
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
1677
|
+
: mesh.nodes[0])
|
|
1678
|
+
: null;
|
|
1679
|
+
const source = explicit || configured;
|
|
1680
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
1681
|
+
? source.workspace.trim()
|
|
1682
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
1683
|
+
? args.workspace.trim()
|
|
1684
|
+
: process.cwd();
|
|
1685
|
+
if (!workspace) return null;
|
|
1686
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
1687
|
+
? source.id.trim()
|
|
1688
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
1689
|
+
? source.nodeId.trim()
|
|
1690
|
+
: undefined;
|
|
1691
|
+
return {
|
|
1692
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
1693
|
+
workspace,
|
|
1694
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
1695
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
1696
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
1697
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
1698
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
1699
|
+
role: 'member',
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
|
|
812
1703
|
export class DaemonCommandRouter {
|
|
813
1704
|
private deps: CommandRouterDeps;
|
|
814
1705
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
815
1706
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
816
1707
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
817
1708
|
private inlineMeshCache = new Map<string, any>();
|
|
1709
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
1710
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
|
|
1711
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
1712
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
1713
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
1714
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
818
1715
|
|
|
819
1716
|
constructor(deps: CommandRouterDeps) {
|
|
820
1717
|
this.deps = deps;
|
|
821
1718
|
}
|
|
822
1719
|
|
|
1720
|
+
private cloneJsonValue<T>(value: T): T {
|
|
1721
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
1722
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
1726
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
1727
|
+
const inlineNodesById = new Map<string, any>();
|
|
1728
|
+
for (const node of mesh.nodes) {
|
|
1729
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
1730
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
1731
|
+
}
|
|
1732
|
+
if (!inlineNodesById.size) return snapshot;
|
|
1733
|
+
|
|
1734
|
+
let changed = false;
|
|
1735
|
+
const unavailableNodeIds = new Set<string>();
|
|
1736
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
1737
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
1738
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
1739
|
+
const nodeId = readStringValue(entry);
|
|
1740
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
1744
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
1745
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
1746
|
+
if (!inlineNode) return statusNode;
|
|
1747
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
1748
|
+
if (!liveGit) return statusNode;
|
|
1749
|
+
const nextStatus = { ...statusNode };
|
|
1750
|
+
nextStatus.git = liveGit;
|
|
1751
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
1752
|
+
applyInlineMeshBranchConvergence(mesh, inlineNode, nextStatus);
|
|
1753
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
1754
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
1755
|
+
const connectionState = readStringValue(connection.state);
|
|
1756
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
1757
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
1758
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
1759
|
+
}
|
|
1760
|
+
delete nextStatus.gitProbePending;
|
|
1761
|
+
const error = readStringValue(nextStatus.error);
|
|
1762
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
1763
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
1764
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
1765
|
+
changed = true;
|
|
1766
|
+
return nextStatus;
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0)) return snapshot;
|
|
1770
|
+
const nextSourceOfTruth = {
|
|
1771
|
+
...sourceOfTruth,
|
|
1772
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
1773
|
+
directPeerTruth: {
|
|
1774
|
+
...directPeerTruth,
|
|
1775
|
+
satisfied: options?.requireDirectPeerTruth === true ? unavailableNodeIds.size === 0 : directPeerTruth.satisfied,
|
|
1776
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
1777
|
+
},
|
|
1778
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
1779
|
+
coordinatorOwnsLiveTruth: unavailableNodeIds.size === 0,
|
|
1780
|
+
currentStatus: unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
1781
|
+
} : {}),
|
|
1782
|
+
} : {}),
|
|
1783
|
+
};
|
|
1784
|
+
return {
|
|
1785
|
+
...snapshot,
|
|
1786
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 ? {
|
|
1787
|
+
success: false,
|
|
1788
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
1789
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
1790
|
+
} : {}),
|
|
1791
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
1792
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodes),
|
|
1793
|
+
nodes,
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
1798
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
1799
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
1800
|
+
if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
|
|
1801
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
1802
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
1803
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
1804
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
1805
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
1806
|
+
? snapshot.sourceOfTruth
|
|
1807
|
+
: {};
|
|
1808
|
+
snapshot.sourceOfTruth = {
|
|
1809
|
+
...sourceOfTruth,
|
|
1810
|
+
aggregateSnapshot: {
|
|
1811
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
1812
|
+
? sourceOfTruth.aggregateSnapshot
|
|
1813
|
+
: {}),
|
|
1814
|
+
owner: 'coordinator_daemon_memory',
|
|
1815
|
+
cached: true,
|
|
1816
|
+
source: 'memory',
|
|
1817
|
+
refreshReason: 'memory_cache_hit',
|
|
1818
|
+
ageMs,
|
|
1819
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
1820
|
+
returnedAt: new Date().toISOString(),
|
|
1821
|
+
},
|
|
1822
|
+
};
|
|
1823
|
+
return snapshot;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
1827
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
1828
|
+
const builtAt = Date.now();
|
|
1829
|
+
const next = this.cloneJsonValue(snapshot);
|
|
1830
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
1831
|
+
? next.sourceOfTruth
|
|
1832
|
+
: {};
|
|
1833
|
+
next.sourceOfTruth = {
|
|
1834
|
+
...sourceOfTruth,
|
|
1835
|
+
aggregateSnapshot: {
|
|
1836
|
+
owner: 'coordinator_daemon_memory',
|
|
1837
|
+
cached: false,
|
|
1838
|
+
source: 'live_refresh',
|
|
1839
|
+
refreshReason,
|
|
1840
|
+
ageMs: 0,
|
|
1841
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
1842
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
1843
|
+
},
|
|
1844
|
+
};
|
|
1845
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
|
|
1846
|
+
return next;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
823
1849
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
824
1850
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
825
|
-
this.
|
|
826
|
-
return inlineMesh as any;
|
|
1851
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
827
1852
|
}
|
|
828
1853
|
return this.inlineMeshCache.get(meshId);
|
|
829
1854
|
}
|
|
830
1855
|
|
|
1856
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
1857
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
1858
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
1859
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
1860
|
+
if (cached) {
|
|
1861
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
1862
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
1863
|
+
return merged;
|
|
1864
|
+
}
|
|
1865
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
1866
|
+
return sanitizedInlineMesh as any;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
831
1869
|
private async getMeshForCommand(
|
|
832
1870
|
meshId: string,
|
|
833
1871
|
inlineMesh?: unknown,
|
|
834
1872
|
options?: { preferInline?: boolean },
|
|
835
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
1873
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
836
1874
|
const preferInline = options?.preferInline === true;
|
|
837
1875
|
if (preferInline) {
|
|
838
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
839
|
-
if (cached)
|
|
1876
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1877
|
+
if (cached) {
|
|
1878
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1879
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
1880
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
1881
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
1882
|
+
}
|
|
1883
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1884
|
+
}
|
|
1885
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
1886
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1887
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
1888
|
+
}
|
|
840
1889
|
}
|
|
841
1890
|
try {
|
|
842
1891
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
843
1892
|
const mesh = getMesh(meshId);
|
|
844
|
-
if (mesh) return { mesh, inline: false };
|
|
1893
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
845
1894
|
} catch { /* fall through to inline cache */ }
|
|
846
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
847
|
-
|
|
1895
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
1896
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
1897
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
1898
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
1902
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
1907
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
1908
|
+
const mesh = meshRecord?.mesh;
|
|
1909
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1910
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
1911
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
1912
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
1913
|
+
}
|
|
1914
|
+
return null;
|
|
848
1915
|
}
|
|
849
1916
|
|
|
850
1917
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -854,6 +1921,7 @@ export class DaemonCommandRouter {
|
|
|
854
1921
|
else mesh.nodes.push(node);
|
|
855
1922
|
mesh.updatedAt = new Date().toISOString();
|
|
856
1923
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1924
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
857
1925
|
}
|
|
858
1926
|
|
|
859
1927
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -863,6 +1931,7 @@ export class DaemonCommandRouter {
|
|
|
863
1931
|
mesh.nodes.splice(idx, 1);
|
|
864
1932
|
mesh.updatedAt = new Date().toISOString();
|
|
865
1933
|
this.inlineMeshCache.set(meshId, mesh);
|
|
1934
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
866
1935
|
return true;
|
|
867
1936
|
}
|
|
868
1937
|
|
|
@@ -1141,6 +2210,7 @@ export class DaemonCommandRouter {
|
|
|
1141
2210
|
const deletedSessionIds: string[] = [];
|
|
1142
2211
|
const skippedSessionIds: string[] = [];
|
|
1143
2212
|
const skippedLiveSessionIds: string[] = [];
|
|
2213
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1144
2214
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1145
2215
|
const recordsRemainSessionIds: string[] = [];
|
|
1146
2216
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1175,6 +2245,12 @@ export class DaemonCommandRouter {
|
|
|
1175
2245
|
const completed = this.isCompletedHostedSession(record);
|
|
1176
2246
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1177
2247
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
2248
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
2249
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
2250
|
+
skippedSessionIds.push(sessionId);
|
|
2251
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
1178
2254
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1179
2255
|
skippedSessionIds.push(sessionId);
|
|
1180
2256
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1244,6 +2320,7 @@ export class DaemonCommandRouter {
|
|
|
1244
2320
|
deletedSessionIds,
|
|
1245
2321
|
skippedSessionIds,
|
|
1246
2322
|
skippedLiveSessionIds,
|
|
2323
|
+
skippedCoordinatorSessionIds,
|
|
1247
2324
|
...(deleteUnsupported ? {
|
|
1248
2325
|
deleteUnsupported: true,
|
|
1249
2326
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1365,23 +2442,430 @@ export class DaemonCommandRouter {
|
|
|
1365
2442
|
payload: { cmd, source: logSource, success: handlerResult.success, durationMs: Date.now() - cmdStart },
|
|
1366
2443
|
});
|
|
1367
2444
|
|
|
1368
|
-
// 3. Post-chat command callback
|
|
1369
|
-
if (CHAT_COMMANDS.includes(cmd) && this.deps.onPostChatCommand) {
|
|
1370
|
-
this.deps.onPostChatCommand();
|
|
2445
|
+
// 3. Post-chat command callback
|
|
2446
|
+
if (CHAT_COMMANDS.includes(cmd) && this.deps.onPostChatCommand) {
|
|
2447
|
+
this.deps.onPostChatCommand();
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
return handlerResult;
|
|
2451
|
+
} catch (e: any) {
|
|
2452
|
+
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: false, error: e.message, durationMs: Date.now() - cmdStart });
|
|
2453
|
+
recordDebugTrace({
|
|
2454
|
+
interactionId,
|
|
2455
|
+
category: 'command',
|
|
2456
|
+
stage: 'failed',
|
|
2457
|
+
level: 'error',
|
|
2458
|
+
payload: { cmd, source: logSource, error: e?.message || String(e), durationMs: Date.now() - cmdStart },
|
|
2459
|
+
});
|
|
2460
|
+
throw e;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
2466
|
+
return `${meshId}:${nodeId}`;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
private buildRefineJobHandle(args: {
|
|
2470
|
+
meshId: string;
|
|
2471
|
+
nodeId: string;
|
|
2472
|
+
node?: any;
|
|
2473
|
+
status?: MeshRefineAsyncJobStatus;
|
|
2474
|
+
startedAt?: string;
|
|
2475
|
+
completedAt?: string;
|
|
2476
|
+
jobId?: string;
|
|
2477
|
+
interactionId?: string;
|
|
2478
|
+
retryOfJobId?: string;
|
|
2479
|
+
}): MeshRefineJobHandle {
|
|
2480
|
+
return {
|
|
2481
|
+
success: true,
|
|
2482
|
+
async: true,
|
|
2483
|
+
status: args.status || 'accepted',
|
|
2484
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
2485
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
2486
|
+
meshId: args.meshId,
|
|
2487
|
+
nodeId: args.nodeId,
|
|
2488
|
+
targetNodeId: args.nodeId,
|
|
2489
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
2490
|
+
workspace: readStringValue(args.node?.workspace),
|
|
2491
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
2492
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
2493
|
+
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
2494
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
2495
|
+
evidence: {
|
|
2496
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
2497
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
2498
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
2499
|
+
},
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
2504
|
+
queuePendingMeshCoordinatorEvent({
|
|
2505
|
+
event,
|
|
2506
|
+
meshId: handle.meshId,
|
|
2507
|
+
nodeLabel: handle.targetNodeId,
|
|
2508
|
+
nodeId: handle.targetNodeId,
|
|
2509
|
+
workspace: handle.workspace,
|
|
2510
|
+
metadataEvent: {
|
|
2511
|
+
source: 'refine_mesh_node_async_job',
|
|
2512
|
+
jobId: handle.jobId,
|
|
2513
|
+
interactionId: handle.interactionId,
|
|
2514
|
+
meshId: handle.meshId,
|
|
2515
|
+
nodeId: handle.targetNodeId,
|
|
2516
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2517
|
+
workspace: handle.workspace,
|
|
2518
|
+
status: handle.status,
|
|
2519
|
+
startedAt: handle.startedAt,
|
|
2520
|
+
completedAt: handle.completedAt,
|
|
2521
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2522
|
+
...(result ? { result } : {}),
|
|
2523
|
+
},
|
|
2524
|
+
queuedAt: Date.now(),
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
2529
|
+
try {
|
|
2530
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2531
|
+
appendLedgerEntry(handle.meshId, {
|
|
2532
|
+
kind,
|
|
2533
|
+
nodeId: handle.targetNodeId,
|
|
2534
|
+
payload: {
|
|
2535
|
+
source: 'refine_mesh_node_async_job',
|
|
2536
|
+
refineJob: {
|
|
2537
|
+
jobId: handle.jobId,
|
|
2538
|
+
interactionId: handle.interactionId,
|
|
2539
|
+
status: handle.status,
|
|
2540
|
+
meshId: handle.meshId,
|
|
2541
|
+
nodeId: handle.targetNodeId,
|
|
2542
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2543
|
+
workspace: handle.workspace,
|
|
2544
|
+
startedAt: handle.startedAt,
|
|
2545
|
+
completedAt: handle.completedAt,
|
|
2546
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2547
|
+
},
|
|
2548
|
+
async: true,
|
|
2549
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2550
|
+
...(result ? {
|
|
2551
|
+
success: result.success === true,
|
|
2552
|
+
result,
|
|
2553
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
2554
|
+
} : {}),
|
|
2555
|
+
},
|
|
2556
|
+
});
|
|
2557
|
+
} catch (e: any) {
|
|
2558
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2563
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
2564
|
+
try {
|
|
2565
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2566
|
+
const mesh = meshRecord?.mesh;
|
|
2567
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2568
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
2569
|
+
|
|
2570
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
2571
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
const sourceNode = node.clonedFromNodeId
|
|
2575
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2576
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2577
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2578
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
2579
|
+
|
|
2580
|
+
const { execFile } = await import('node:child_process');
|
|
2581
|
+
const { promisify } = await import('node:util');
|
|
2582
|
+
const execFileAsync = promisify(execFile);
|
|
2583
|
+
|
|
2584
|
+
const resolveStarted = Date.now();
|
|
2585
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2586
|
+
const branch = branchStdout.trim();
|
|
2587
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
2588
|
+
|
|
2589
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2590
|
+
const baseBranch = baseBranchStdout.trim();
|
|
2591
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2592
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
2593
|
+
const baseHead = baseHeadStdout.trim();
|
|
2594
|
+
const branchHead = branchHeadStdout.trim();
|
|
2595
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
2596
|
+
|
|
2597
|
+
const validationStarted = Date.now();
|
|
2598
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2599
|
+
recordMeshRefineStage(
|
|
2600
|
+
refineStages,
|
|
2601
|
+
'validation',
|
|
2602
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
2603
|
+
validationStarted,
|
|
2604
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
2605
|
+
);
|
|
2606
|
+
if (validationSummary.status === 'failed') {
|
|
2607
|
+
return {
|
|
2608
|
+
success: false,
|
|
2609
|
+
code: 'validation_failed',
|
|
2610
|
+
convergenceStatus: 'blocked_review',
|
|
2611
|
+
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2612
|
+
branch,
|
|
2613
|
+
into: baseBranch,
|
|
2614
|
+
validationSummary,
|
|
2615
|
+
refineStages,
|
|
2616
|
+
finalBranchConvergenceState: {
|
|
2617
|
+
branch,
|
|
2618
|
+
baseBranch,
|
|
2619
|
+
merged: false,
|
|
2620
|
+
removed: false,
|
|
2621
|
+
validation: 'failed',
|
|
2622
|
+
status: 'blocked_review',
|
|
2623
|
+
},
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
if (validationSummary.status === 'skipped') {
|
|
2627
|
+
return {
|
|
2628
|
+
success: false,
|
|
2629
|
+
code: 'validation_unavailable',
|
|
2630
|
+
convergenceStatus: 'blocked_review',
|
|
2631
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2632
|
+
branch,
|
|
2633
|
+
into: baseBranch,
|
|
2634
|
+
validationSummary,
|
|
2635
|
+
refineStages,
|
|
2636
|
+
finalBranchConvergenceState: {
|
|
2637
|
+
branch,
|
|
2638
|
+
baseBranch,
|
|
2639
|
+
merged: false,
|
|
2640
|
+
removed: false,
|
|
2641
|
+
validation: 'unavailable',
|
|
2642
|
+
status: 'blocked_review',
|
|
2643
|
+
},
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
const patchEquivalenceStarted = Date.now();
|
|
2648
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
2649
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
2650
|
+
equivalent: patchEquivalence.equivalent,
|
|
2651
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
2652
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
2653
|
+
error: patchEquivalence.error,
|
|
2654
|
+
});
|
|
2655
|
+
if (!patchEquivalence.equivalent) {
|
|
2656
|
+
return {
|
|
2657
|
+
success: false,
|
|
2658
|
+
code: 'patch_equivalence_failed',
|
|
2659
|
+
convergenceStatus: 'blocked_review',
|
|
2660
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
2661
|
+
branch,
|
|
2662
|
+
into: baseBranch,
|
|
2663
|
+
validationSummary,
|
|
2664
|
+
patchEquivalence,
|
|
2665
|
+
refineStages,
|
|
2666
|
+
finalBranchConvergenceState: {
|
|
2667
|
+
branch,
|
|
2668
|
+
baseBranch,
|
|
2669
|
+
merged: false,
|
|
2670
|
+
removed: false,
|
|
2671
|
+
validation: 'passed',
|
|
2672
|
+
patchEquivalence: 'failed',
|
|
2673
|
+
status: 'blocked_review',
|
|
2674
|
+
},
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
const submoduleReachabilityStarted = Date.now();
|
|
2679
|
+
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead);
|
|
2680
|
+
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
2681
|
+
checked: submoduleReachability.checked,
|
|
2682
|
+
unreachable: submoduleReachability.unreachable.map(entry => ({ path: entry.path, commit: entry.commit, error: entry.error })),
|
|
2683
|
+
error: submoduleReachability.error,
|
|
2684
|
+
});
|
|
2685
|
+
if (submoduleReachability.status === 'failed') {
|
|
2686
|
+
return {
|
|
2687
|
+
success: false,
|
|
2688
|
+
code: 'submodule_reachability_failed',
|
|
2689
|
+
convergenceStatus: 'blocked_review',
|
|
2690
|
+
error: 'Refinery submodule reachability preflight failed; merge/refine cleanup was not attempted.',
|
|
2691
|
+
branch,
|
|
2692
|
+
into: baseBranch,
|
|
2693
|
+
validationSummary,
|
|
2694
|
+
patchEquivalence,
|
|
2695
|
+
submoduleReachability,
|
|
2696
|
+
refineStages,
|
|
2697
|
+
finalBranchConvergenceState: {
|
|
2698
|
+
branch,
|
|
2699
|
+
baseBranch,
|
|
2700
|
+
merged: false,
|
|
2701
|
+
removed: false,
|
|
2702
|
+
validation: 'passed',
|
|
2703
|
+
patchEquivalence: 'passed',
|
|
2704
|
+
submoduleReachability: 'failed',
|
|
2705
|
+
status: 'blocked_review',
|
|
2706
|
+
},
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
2711
|
+
const mergeStarted = Date.now();
|
|
2712
|
+
try {
|
|
2713
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2714
|
+
mergeResult = {
|
|
2715
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
2716
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
2717
|
+
durationMs: Date.now() - mergeStarted,
|
|
2718
|
+
};
|
|
2719
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
2720
|
+
} catch (e: any) {
|
|
2721
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
2722
|
+
error: e?.message || String(e),
|
|
2723
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
2724
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
2725
|
+
});
|
|
2726
|
+
return {
|
|
2727
|
+
success: false,
|
|
2728
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2729
|
+
validationSummary,
|
|
2730
|
+
patchEquivalence,
|
|
2731
|
+
refineStages,
|
|
2732
|
+
finalBranchConvergenceState: {
|
|
2733
|
+
branch,
|
|
2734
|
+
baseBranch,
|
|
2735
|
+
merged: false,
|
|
2736
|
+
removed: false,
|
|
2737
|
+
validation: 'passed',
|
|
2738
|
+
patchEquivalence: 'passed',
|
|
2739
|
+
status: 'not_mergeable',
|
|
2740
|
+
},
|
|
2741
|
+
};
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
const cleanupStarted = Date.now();
|
|
2745
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
2746
|
+
meshId,
|
|
2747
|
+
nodeId,
|
|
2748
|
+
sessionCleanupMode: 'preserve',
|
|
2749
|
+
inlineMesh: args?.inlineMesh,
|
|
2750
|
+
});
|
|
2751
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
2752
|
+
removed: removeResult?.removed,
|
|
2753
|
+
code: removeResult?.code,
|
|
2754
|
+
error: removeResult?.error,
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2757
|
+
let ledgerError: string | undefined;
|
|
2758
|
+
const ledgerStarted = Date.now();
|
|
2759
|
+
try {
|
|
2760
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2761
|
+
appendLedgerEntry(meshId, {
|
|
2762
|
+
kind: 'node_removed',
|
|
2763
|
+
nodeId,
|
|
2764
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
2765
|
+
});
|
|
2766
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
2767
|
+
} catch (e: any) {
|
|
2768
|
+
ledgerError = e?.message || String(e);
|
|
2769
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
1371
2770
|
}
|
|
1372
2771
|
|
|
1373
|
-
|
|
2772
|
+
const finalBranchConvergenceState = {
|
|
2773
|
+
branch: baseBranch,
|
|
2774
|
+
mergedBranch: branch,
|
|
2775
|
+
baseBranch,
|
|
2776
|
+
merged: true,
|
|
2777
|
+
removed: removeResult?.success !== false,
|
|
2778
|
+
validation: 'passed',
|
|
2779
|
+
patchEquivalence: 'passed',
|
|
2780
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2781
|
+
};
|
|
2782
|
+
|
|
2783
|
+
if (removeResult?.success === false) {
|
|
2784
|
+
return {
|
|
2785
|
+
success: false,
|
|
2786
|
+
code: 'cleanup_failed',
|
|
2787
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
2788
|
+
merged: true,
|
|
2789
|
+
branch,
|
|
2790
|
+
into: baseBranch,
|
|
2791
|
+
removeResult,
|
|
2792
|
+
validationSummary,
|
|
2793
|
+
patchEquivalence,
|
|
2794
|
+
mergeResult,
|
|
2795
|
+
refineStages,
|
|
2796
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2797
|
+
finalBranchConvergenceState,
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
return {
|
|
2802
|
+
success: true,
|
|
2803
|
+
merged: true,
|
|
2804
|
+
branch,
|
|
2805
|
+
into: baseBranch,
|
|
2806
|
+
removeResult,
|
|
2807
|
+
validationSummary,
|
|
2808
|
+
patchEquivalence,
|
|
2809
|
+
mergeResult,
|
|
2810
|
+
refineStages,
|
|
2811
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
2812
|
+
finalBranchConvergenceState,
|
|
2813
|
+
};
|
|
1374
2814
|
} catch (e: any) {
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
2815
|
+
return { success: false, error: e.message, refineStages };
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
2820
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
2821
|
+
let result: Record<string, unknown>;
|
|
2822
|
+
try {
|
|
2823
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
2824
|
+
} catch (e: any) {
|
|
2825
|
+
result = { success: false, error: e?.message || String(e) };
|
|
1384
2826
|
}
|
|
2827
|
+
const completedAt = new Date().toISOString();
|
|
2828
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
2829
|
+
meshId: handle.meshId,
|
|
2830
|
+
nodeId: handle.targetNodeId,
|
|
2831
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
2832
|
+
startedAt: handle.startedAt,
|
|
2833
|
+
completedAt,
|
|
2834
|
+
jobId: handle.jobId,
|
|
2835
|
+
interactionId: handle.interactionId,
|
|
2836
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2837
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
2838
|
+
});
|
|
2839
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
2840
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
2841
|
+
this.runningRefineJobs.delete(key);
|
|
2842
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
2843
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
2844
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2848
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
2849
|
+
const running = this.runningRefineJobs.get(key);
|
|
2850
|
+
if (running) return { ...running, duplicate: true };
|
|
2851
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
2852
|
+
|
|
2853
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2854
|
+
const mesh = meshRecord?.mesh;
|
|
2855
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2856
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
2857
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
2858
|
+
|
|
2859
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
|
|
2860
|
+
this.runningRefineJobs.set(key, handle);
|
|
2861
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
2862
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
2863
|
+
|
|
2864
|
+
setImmediate(() => {
|
|
2865
|
+
void this.finishMeshRefineJob(handle, args);
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
return handle;
|
|
1385
2869
|
}
|
|
1386
2870
|
|
|
1387
2871
|
// ─── Daemon-level command core ───────────────────
|
|
@@ -1398,7 +2882,8 @@ export class DaemonCommandRouter {
|
|
|
1398
2882
|
}
|
|
1399
2883
|
|
|
1400
2884
|
case 'get_pending_mesh_events': {
|
|
1401
|
-
const
|
|
2885
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2886
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1402
2887
|
return { success: true, events };
|
|
1403
2888
|
}
|
|
1404
2889
|
|
|
@@ -2003,15 +3488,44 @@ export class DaemonCommandRouter {
|
|
|
2003
3488
|
case 'get_mesh': {
|
|
2004
3489
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2005
3490
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
3491
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3492
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
3493
|
+
|
|
3494
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
3495
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
3496
|
+
mesh: meshRecord.mesh,
|
|
3497
|
+
meshSource: meshRecord.source,
|
|
3498
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
3499
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
3500
|
+
localMachineId: loadConfig().machineId || '',
|
|
3501
|
+
});
|
|
3502
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
3503
|
+
const sourceOfTruth = {
|
|
3504
|
+
membership: meshRecord.source === 'inline_cache'
|
|
3505
|
+
? 'coordinator_inline_mesh_cache'
|
|
3506
|
+
: meshRecord.source === 'local_config'
|
|
3507
|
+
? 'local_mesh_config'
|
|
3508
|
+
: 'inline_bootstrap_snapshot',
|
|
3509
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
3510
|
+
directPeerTruth: {
|
|
3511
|
+
required: requireDirectPeerTruth,
|
|
3512
|
+
satisfied: directTruthSatisfied,
|
|
3513
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
3514
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
3515
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
3516
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
3517
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
3518
|
+
},
|
|
3519
|
+
};
|
|
3520
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
3521
|
+
return {
|
|
3522
|
+
success: false,
|
|
3523
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
3524
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
3525
|
+
sourceOfTruth,
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
2015
3529
|
}
|
|
2016
3530
|
|
|
2017
3531
|
case 'create_mesh': {
|
|
@@ -2022,7 +3536,10 @@ export class DaemonCommandRouter {
|
|
|
2022
3536
|
if (!name) return { success: false, error: 'name required' };
|
|
2023
3537
|
try {
|
|
2024
3538
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
2025
|
-
const
|
|
3539
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
3540
|
+
? args.meshHost
|
|
3541
|
+
: undefined;
|
|
3542
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
2026
3543
|
return { success: true, mesh };
|
|
2027
3544
|
} catch (e: any) {
|
|
2028
3545
|
return { success: false, error: e.message };
|
|
@@ -2039,16 +3556,237 @@ export class DaemonCommandRouter {
|
|
|
2039
3556
|
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
2040
3557
|
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
2041
3558
|
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
3559
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
2042
3560
|
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
2043
3561
|
const mesh = updateMesh(meshId, patch as any);
|
|
2044
3562
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2045
3563
|
this.inlineMeshCache.set(meshId, mesh);
|
|
3564
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2046
3565
|
return { success: true, mesh };
|
|
2047
3566
|
} catch (e: any) {
|
|
2048
3567
|
return { success: false, error: e.message };
|
|
2049
3568
|
}
|
|
2050
3569
|
}
|
|
2051
3570
|
|
|
3571
|
+
case 'get_mesh_host_pairing': {
|
|
3572
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3573
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3574
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3575
|
+
const mesh = meshRecord?.mesh;
|
|
3576
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3577
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3578
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
3579
|
+
return {
|
|
3580
|
+
success: true,
|
|
3581
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
3582
|
+
meshId,
|
|
3583
|
+
hostAddress: meshHost.hostAddress,
|
|
3584
|
+
meshHost,
|
|
3585
|
+
manualPairing: {
|
|
3586
|
+
status: pairingStatus,
|
|
3587
|
+
joinImplemented: true,
|
|
3588
|
+
protocol: 'standalone_command_direct_v1',
|
|
3589
|
+
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.',
|
|
3590
|
+
},
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
case 'configure_mesh_host_pairing': {
|
|
3595
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3596
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
3597
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3598
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3599
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
3600
|
+
try {
|
|
3601
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
3602
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
3603
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
3604
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
3605
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
3606
|
+
return {
|
|
3607
|
+
success: true,
|
|
3608
|
+
code: 'mesh_host_pairing_pending',
|
|
3609
|
+
meshId,
|
|
3610
|
+
hostAddress: configured.hostAddress,
|
|
3611
|
+
meshHost,
|
|
3612
|
+
manualPairing: {
|
|
3613
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
3614
|
+
joinImplemented: true,
|
|
3615
|
+
protocol: 'standalone_command_direct_v1',
|
|
3616
|
+
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.',
|
|
3617
|
+
},
|
|
3618
|
+
};
|
|
3619
|
+
} catch (e: any) {
|
|
3620
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
case 'create_mesh_host_pairing_token': {
|
|
3625
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3626
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3627
|
+
try {
|
|
3628
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
3629
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
3630
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
3631
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
3632
|
+
});
|
|
3633
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
3634
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
3635
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3636
|
+
return {
|
|
3637
|
+
success: true,
|
|
3638
|
+
code: 'mesh_host_pairing_token_created',
|
|
3639
|
+
meshId,
|
|
3640
|
+
token: created.token,
|
|
3641
|
+
tokenId: created.tokenId,
|
|
3642
|
+
expiresAt: created.expiresAt,
|
|
3643
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
3644
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
3645
|
+
};
|
|
3646
|
+
} catch (e: any) {
|
|
3647
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
case 'apply_mesh_host_join': {
|
|
3652
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3653
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3654
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
3655
|
+
? args.memberNode
|
|
3656
|
+
: null;
|
|
3657
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3658
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
3659
|
+
try {
|
|
3660
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
3661
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
3662
|
+
token,
|
|
3663
|
+
memberNode: memberNode as any,
|
|
3664
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
3665
|
+
});
|
|
3666
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
3667
|
+
if (!applied.accepted) {
|
|
3668
|
+
return {
|
|
3669
|
+
success: false,
|
|
3670
|
+
code: 'mesh_host_join_rejected',
|
|
3671
|
+
meshId,
|
|
3672
|
+
tokenId: applied.tokenId,
|
|
3673
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
3674
|
+
error: applied.reason,
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3677
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
3678
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3679
|
+
try {
|
|
3680
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3681
|
+
appendLedgerEntry(meshId, {
|
|
3682
|
+
kind: 'node_joined',
|
|
3683
|
+
nodeId: applied.node.id,
|
|
3684
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
3685
|
+
});
|
|
3686
|
+
} catch { /* ledger append is best-effort */ }
|
|
3687
|
+
return {
|
|
3688
|
+
success: true,
|
|
3689
|
+
code: 'mesh_host_join_accepted',
|
|
3690
|
+
meshId,
|
|
3691
|
+
node: applied.node,
|
|
3692
|
+
tokenId: applied.tokenId,
|
|
3693
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
3694
|
+
};
|
|
3695
|
+
} catch (e: any) {
|
|
3696
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
case 'join_mesh_host_pairing': {
|
|
3701
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3702
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
3703
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
3704
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
3705
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3706
|
+
const mesh = meshRecord?.mesh;
|
|
3707
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
3708
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
3709
|
+
if (meshHost.role !== 'member') {
|
|
3710
|
+
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.' };
|
|
3711
|
+
}
|
|
3712
|
+
try {
|
|
3713
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
3714
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
3715
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
3716
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
3717
|
+
}
|
|
3718
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
3719
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
3720
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
3721
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
3722
|
+
? args.hostDaemonId.trim()
|
|
3723
|
+
: meshHost.hostDaemonId;
|
|
3724
|
+
let hostResult: any;
|
|
3725
|
+
let transport: string;
|
|
3726
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
3727
|
+
transport = 'mesh_command_dispatch';
|
|
3728
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
3729
|
+
meshId: hostMeshId,
|
|
3730
|
+
token,
|
|
3731
|
+
memberMeshId: meshId,
|
|
3732
|
+
memberNode,
|
|
3733
|
+
});
|
|
3734
|
+
} else if (meshHost.hostAddress) {
|
|
3735
|
+
transport = 'standalone_http_command';
|
|
3736
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
3737
|
+
const response = await fetch(commandUrl, {
|
|
3738
|
+
method: 'POST',
|
|
3739
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3740
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
3741
|
+
});
|
|
3742
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
3743
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
3744
|
+
} else {
|
|
3745
|
+
return {
|
|
3746
|
+
success: false,
|
|
3747
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
3748
|
+
meshId,
|
|
3749
|
+
meshHost,
|
|
3750
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
3753
|
+
if (!hostResult?.success) {
|
|
3754
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
3755
|
+
}
|
|
3756
|
+
const joined = meshRecord.inline
|
|
3757
|
+
? null
|
|
3758
|
+
: markMeshHostPairingJoined(meshId, {
|
|
3759
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3760
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
3761
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
3762
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
3763
|
+
});
|
|
3764
|
+
if (joined) {
|
|
3765
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
3766
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
3767
|
+
}
|
|
3768
|
+
return {
|
|
3769
|
+
success: true,
|
|
3770
|
+
code: 'mesh_host_join_applied',
|
|
3771
|
+
meshId,
|
|
3772
|
+
hostMeshId,
|
|
3773
|
+
transport,
|
|
3774
|
+
node: hostResult.node,
|
|
3775
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
3776
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
3777
|
+
hostResult,
|
|
3778
|
+
manualPairing: {
|
|
3779
|
+
status: 'paired',
|
|
3780
|
+
joinImplemented: true,
|
|
3781
|
+
protocol: 'standalone_command_direct_v1',
|
|
3782
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
3783
|
+
},
|
|
3784
|
+
};
|
|
3785
|
+
} catch (e: any) {
|
|
3786
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
|
|
2052
3790
|
case 'delete_mesh': {
|
|
2053
3791
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2054
3792
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -2142,6 +3880,8 @@ export class DaemonCommandRouter {
|
|
|
2142
3880
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2143
3881
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2144
3882
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3883
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
3884
|
+
if (ownerFailure) return ownerFailure;
|
|
2145
3885
|
try {
|
|
2146
3886
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2147
3887
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -2157,6 +3897,8 @@ export class DaemonCommandRouter {
|
|
|
2157
3897
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2158
3898
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2159
3899
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
3900
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
3901
|
+
if (ownerFailure) return ownerFailure;
|
|
2160
3902
|
try {
|
|
2161
3903
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2162
3904
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -2178,6 +3920,8 @@ export class DaemonCommandRouter {
|
|
|
2178
3920
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2179
3921
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2180
3922
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
3923
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
3924
|
+
if (ownerFailure) return ownerFailure;
|
|
2181
3925
|
try {
|
|
2182
3926
|
const { addNode } = await import('../config/mesh-config.js');
|
|
2183
3927
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -2188,7 +3932,18 @@ export class DaemonCommandRouter {
|
|
|
2188
3932
|
...(readOnly ? { readOnly: true } : {}),
|
|
2189
3933
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
2190
3934
|
};
|
|
2191
|
-
const
|
|
3935
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
3936
|
+
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
3937
|
+
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
3938
|
+
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
3939
|
+
const node = addNode(meshId, {
|
|
3940
|
+
workspace,
|
|
3941
|
+
...(repoRoot ? { repoRoot } : {}),
|
|
3942
|
+
...(daemonId ? { daemonId } : {}),
|
|
3943
|
+
...(machineId ? { machineId } : {}),
|
|
3944
|
+
...(policy ? { policy } : {}),
|
|
3945
|
+
...(role ? { role } : {}),
|
|
3946
|
+
});
|
|
2192
3947
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
2193
3948
|
return { success: true, node };
|
|
2194
3949
|
} catch (e: any) {
|
|
@@ -2200,6 +3955,8 @@ export class DaemonCommandRouter {
|
|
|
2200
3955
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2201
3956
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2202
3957
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3958
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
3959
|
+
if (ownerFailure) return ownerFailure;
|
|
2203
3960
|
try {
|
|
2204
3961
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2205
3962
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2228,6 +3985,8 @@ export class DaemonCommandRouter {
|
|
|
2228
3985
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2229
3986
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2230
3987
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
3988
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
3989
|
+
if (ownerFailure) return ownerFailure;
|
|
2231
3990
|
try {
|
|
2232
3991
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2233
3992
|
const mesh = meshRecord?.mesh;
|
|
@@ -2253,131 +4012,91 @@ export class DaemonCommandRouter {
|
|
|
2253
4012
|
}
|
|
2254
4013
|
}
|
|
2255
4014
|
|
|
2256
|
-
case '
|
|
4015
|
+
case 'get_mesh_refine_config_schema': {
|
|
4016
|
+
return {
|
|
4017
|
+
success: true,
|
|
4018
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
4019
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
4020
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
4021
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
4022
|
+
};
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
case 'validate_mesh_refine_config': {
|
|
4026
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4027
|
+
const mesh = args?.inlineMesh || {};
|
|
4028
|
+
const loaded = args?.config !== undefined
|
|
4029
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
4030
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
4031
|
+
const validation = loaded.config
|
|
4032
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
4033
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
4034
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
case 'suggest_mesh_refine_config': {
|
|
4038
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4039
|
+
const mesh = args?.inlineMesh || {};
|
|
4040
|
+
return {
|
|
4041
|
+
success: true,
|
|
4042
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
4043
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
case 'plan_mesh_refine_node': {
|
|
2257
4048
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2258
4049
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2259
4050
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2260
|
-
|
|
4051
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
4052
|
+
const mesh = meshRecord?.mesh;
|
|
4053
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
4054
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
4055
|
+
return {
|
|
4056
|
+
success: true,
|
|
4057
|
+
dryRun: true,
|
|
4058
|
+
nodeId,
|
|
4059
|
+
workspace: node.workspace,
|
|
4060
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
4061
|
+
mergeWillRun: false,
|
|
4062
|
+
cleanupWillRun: false,
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
case 'fast_forward_mesh_node': {
|
|
4067
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4068
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4069
|
+
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
4070
|
+
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
4071
|
+
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
4072
|
+
: undefined;
|
|
4073
|
+
if (!workspace && meshId && nodeId) {
|
|
2261
4074
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2262
4075
|
const mesh = meshRecord?.mesh;
|
|
2263
4076
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
return { success: false, error: `Refinery requires a local worktree node` };
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
const sourceNode = node.clonedFromNodeId
|
|
2271
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2272
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2273
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2274
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2275
|
-
|
|
2276
|
-
const { execFile } = await import('node:child_process');
|
|
2277
|
-
const { promisify } = await import('node:util');
|
|
2278
|
-
const execFileAsync = promisify(execFile);
|
|
2279
|
-
|
|
2280
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2281
|
-
const branch = branchStdout.trim();
|
|
2282
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2283
|
-
|
|
2284
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2285
|
-
const baseBranch = baseBranchStdout.trim();
|
|
2286
|
-
|
|
2287
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2288
|
-
if (validationSummary.status === 'failed') {
|
|
2289
|
-
return {
|
|
2290
|
-
success: false,
|
|
2291
|
-
code: 'validation_failed',
|
|
2292
|
-
convergenceStatus: 'blocked_review',
|
|
2293
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2294
|
-
branch,
|
|
2295
|
-
into: baseBranch,
|
|
2296
|
-
validationSummary,
|
|
2297
|
-
finalBranchConvergenceState: {
|
|
2298
|
-
branch,
|
|
2299
|
-
baseBranch,
|
|
2300
|
-
merged: false,
|
|
2301
|
-
removed: false,
|
|
2302
|
-
validation: 'failed',
|
|
2303
|
-
status: 'blocked_review',
|
|
2304
|
-
},
|
|
2305
|
-
};
|
|
4077
|
+
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
4078
|
+
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
4079
|
+
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
2306
4080
|
}
|
|
2307
|
-
if (validationSummary.status === 'skipped') {
|
|
2308
|
-
return {
|
|
2309
|
-
success: false,
|
|
2310
|
-
code: 'validation_unavailable',
|
|
2311
|
-
convergenceStatus: 'blocked_review',
|
|
2312
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2313
|
-
branch,
|
|
2314
|
-
into: baseBranch,
|
|
2315
|
-
validationSummary,
|
|
2316
|
-
finalBranchConvergenceState: {
|
|
2317
|
-
branch,
|
|
2318
|
-
baseBranch,
|
|
2319
|
-
merged: false,
|
|
2320
|
-
removed: false,
|
|
2321
|
-
validation: 'unavailable',
|
|
2322
|
-
status: 'blocked_review',
|
|
2323
|
-
},
|
|
2324
|
-
};
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
try {
|
|
2328
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2329
|
-
} catch (e: any) {
|
|
2330
|
-
return {
|
|
2331
|
-
success: false,
|
|
2332
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2333
|
-
validationSummary,
|
|
2334
|
-
finalBranchConvergenceState: {
|
|
2335
|
-
branch,
|
|
2336
|
-
baseBranch,
|
|
2337
|
-
merged: false,
|
|
2338
|
-
removed: false,
|
|
2339
|
-
validation: 'passed',
|
|
2340
|
-
status: 'not_mergeable',
|
|
2341
|
-
},
|
|
2342
|
-
};
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
2346
|
-
meshId,
|
|
2347
|
-
nodeId,
|
|
2348
|
-
sessionCleanupMode: 'kill',
|
|
2349
|
-
inlineMesh: args?.inlineMesh,
|
|
2350
|
-
});
|
|
2351
|
-
|
|
2352
|
-
try {
|
|
2353
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2354
|
-
appendLedgerEntry(meshId, {
|
|
2355
|
-
kind: 'node_removed',
|
|
2356
|
-
nodeId,
|
|
2357
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
2358
|
-
});
|
|
2359
|
-
} catch {}
|
|
2360
|
-
|
|
2361
|
-
return {
|
|
2362
|
-
success: true,
|
|
2363
|
-
merged: true,
|
|
2364
|
-
branch,
|
|
2365
|
-
into: baseBranch,
|
|
2366
|
-
removeResult,
|
|
2367
|
-
validationSummary,
|
|
2368
|
-
finalBranchConvergenceState: {
|
|
2369
|
-
branch: baseBranch,
|
|
2370
|
-
mergedBranch: branch,
|
|
2371
|
-
baseBranch,
|
|
2372
|
-
merged: true,
|
|
2373
|
-
removed: removeResult?.success !== false,
|
|
2374
|
-
validation: 'passed',
|
|
2375
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2376
|
-
},
|
|
2377
|
-
};
|
|
2378
|
-
} catch (e: any) {
|
|
2379
|
-
return { success: false, error: e.message };
|
|
2380
4081
|
}
|
|
4082
|
+
const result = await (fastForwardMeshNode({
|
|
4083
|
+
meshId: meshId || undefined,
|
|
4084
|
+
nodeId: nodeId || undefined,
|
|
4085
|
+
workspace,
|
|
4086
|
+
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
4087
|
+
execute: args?.execute === true,
|
|
4088
|
+
dryRun: args?.dryRun === true,
|
|
4089
|
+
updateSubmodules: args?.updateSubmodules === true,
|
|
4090
|
+
submoduleIgnorePaths,
|
|
4091
|
+
}) as Promise<unknown>);
|
|
4092
|
+
return result as CommandRouterResult;
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
case 'refine_mesh_node': {
|
|
4096
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4097
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4098
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4099
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
2381
4100
|
}
|
|
2382
4101
|
|
|
2383
4102
|
case 'remove_mesh_node': {
|
|
@@ -2421,6 +4140,7 @@ export class DaemonCommandRouter {
|
|
|
2421
4140
|
} else {
|
|
2422
4141
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2423
4142
|
removed = removeNode(meshId, nodeId);
|
|
4143
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2424
4144
|
}
|
|
2425
4145
|
|
|
2426
4146
|
// Record in task ledger
|
|
@@ -2458,6 +4178,8 @@ export class DaemonCommandRouter {
|
|
|
2458
4178
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2459
4179
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2460
4180
|
if (!branch) return { success: false, error: 'branch required' };
|
|
4181
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
4182
|
+
if (ownerFailure) return ownerFailure;
|
|
2461
4183
|
|
|
2462
4184
|
try {
|
|
2463
4185
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2506,6 +4228,7 @@ export class DaemonCommandRouter {
|
|
|
2506
4228
|
policy: { ...(sourceNode.policy || {}) },
|
|
2507
4229
|
});
|
|
2508
4230
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
4231
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2509
4232
|
}
|
|
2510
4233
|
|
|
2511
4234
|
// Initialize submodules if policy allows (default: true)
|
|
@@ -2547,6 +4270,8 @@ export class DaemonCommandRouter {
|
|
|
2547
4270
|
case 'trigger_mesh_queue': {
|
|
2548
4271
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2549
4272
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4273
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
4274
|
+
if (ownerFailure) return ownerFailure;
|
|
2550
4275
|
try {
|
|
2551
4276
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2552
4277
|
if (meshId) {
|
|
@@ -2578,6 +4303,15 @@ export class DaemonCommandRouter {
|
|
|
2578
4303
|
mesh = getMesh(meshId);
|
|
2579
4304
|
}
|
|
2580
4305
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4306
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4307
|
+
if (!meshHost.canOwnCoordinator) {
|
|
4308
|
+
return {
|
|
4309
|
+
success: false,
|
|
4310
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
4311
|
+
meshId,
|
|
4312
|
+
cliType,
|
|
4313
|
+
};
|
|
4314
|
+
}
|
|
2581
4315
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2582
4316
|
|
|
2583
4317
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2597,7 +4331,16 @@ export class DaemonCommandRouter {
|
|
|
2597
4331
|
cliType,
|
|
2598
4332
|
};
|
|
2599
4333
|
}
|
|
2600
|
-
const
|
|
4334
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
4335
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
4336
|
+
: [];
|
|
4337
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
4338
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4339
|
+
meshId,
|
|
4340
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
4341
|
+
liveSessionRecords: liveMeshSessions,
|
|
4342
|
+
allowCoordinatorSession: true,
|
|
4343
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2601
4344
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2602
4345
|
if (!cliType) {
|
|
2603
4346
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -2942,6 +4685,27 @@ export class DaemonCommandRouter {
|
|
|
2942
4685
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2943
4686
|
const mesh = meshRecord?.mesh;
|
|
2944
4687
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4688
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4689
|
+
|
|
4690
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
4691
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
4692
|
+
if (!refreshRequested) {
|
|
4693
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
4694
|
+
if (cachedStatus) {
|
|
4695
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
4696
|
+
meshId,
|
|
4697
|
+
command: 'mesh_status',
|
|
4698
|
+
refreshRequested,
|
|
4699
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
4700
|
+
});
|
|
4701
|
+
return cachedStatus;
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
const refreshReason = refreshRequested
|
|
4705
|
+
? 'explicit_refresh'
|
|
4706
|
+
: hadAggregateCache
|
|
4707
|
+
? 'stale_pending_cache_refresh'
|
|
4708
|
+
: 'cold_cache_miss';
|
|
2945
4709
|
|
|
2946
4710
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
2947
4711
|
const queue = getQueue(meshId);
|
|
@@ -2955,64 +4719,343 @@ export class DaemonCommandRouter {
|
|
|
2955
4719
|
: [];
|
|
2956
4720
|
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
2957
4721
|
|
|
4722
|
+
const localMachineId = loadConfig().machineId || '';
|
|
4723
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
4724
|
+
const directTruth = requireDirectPeerTruth
|
|
4725
|
+
? await hydrateInlineMeshDirectTruth({
|
|
4726
|
+
mesh,
|
|
4727
|
+
meshSource: meshRecord.source,
|
|
4728
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4729
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
4730
|
+
localMachineId,
|
|
4731
|
+
})
|
|
4732
|
+
: {
|
|
4733
|
+
directEvidenceCount: 0,
|
|
4734
|
+
localConfirmedCount: 0,
|
|
4735
|
+
peerAttemptedCount: 0,
|
|
4736
|
+
peerConfirmedCount: 0,
|
|
4737
|
+
unavailableNodeIds: [] as string[],
|
|
4738
|
+
};
|
|
4739
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
4740
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
4741
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
4742
|
+
&& !refreshRequested
|
|
4743
|
+
&& directTruth.directEvidenceCount > 0
|
|
4744
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
4745
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
4746
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
4747
|
+
: directTruth;
|
|
4748
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
4749
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && effectiveDirectTruth.unavailableNodeIds.length === 0);
|
|
4750
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4751
|
+
const failureResult = {
|
|
4752
|
+
success: false,
|
|
4753
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
4754
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
4755
|
+
sourceOfTruth: {
|
|
4756
|
+
membership: meshRecord.source === 'inline_cache'
|
|
4757
|
+
? 'coordinator_inline_mesh_cache'
|
|
4758
|
+
: meshRecord.source === 'local_config'
|
|
4759
|
+
? 'local_mesh_config'
|
|
4760
|
+
: 'inline_bootstrap_snapshot',
|
|
4761
|
+
coordinatorOwnsLiveTruth: false,
|
|
4762
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
4763
|
+
directPeerTruth: {
|
|
4764
|
+
required: true,
|
|
4765
|
+
satisfied: false,
|
|
4766
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4767
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4768
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4769
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4770
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4771
|
+
},
|
|
4772
|
+
},
|
|
4773
|
+
};
|
|
4774
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
4775
|
+
meshId,
|
|
4776
|
+
command: 'mesh_status',
|
|
4777
|
+
refreshRequested,
|
|
4778
|
+
meshSource: meshRecord.source,
|
|
4779
|
+
directTruth,
|
|
4780
|
+
});
|
|
4781
|
+
return failureResult;
|
|
4782
|
+
}
|
|
4783
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
4784
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
4785
|
+
mesh.coordinator?.preferredNodeId,
|
|
4786
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
4787
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
4788
|
+
);
|
|
4789
|
+
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
4790
|
+
? selectedCoordinatorNodeId
|
|
4791
|
+
: undefined;
|
|
4792
|
+
const refreshedAt = new Date().toISOString();
|
|
2958
4793
|
const nodeStatuses = [];
|
|
2959
|
-
for (const node of mesh.nodes || []) {
|
|
4794
|
+
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
4795
|
+
const nodeId = String(node.id || node.nodeId || '');
|
|
4796
|
+
const daemonId = readStringValue(node.daemonId);
|
|
4797
|
+
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
4798
|
+
const isSelfNode = Boolean(
|
|
4799
|
+
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
4800
|
+
) || Boolean(
|
|
4801
|
+
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
4802
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
2960
4803
|
const status: Record<string, unknown> = {
|
|
2961
|
-
nodeId
|
|
4804
|
+
nodeId,
|
|
2962
4805
|
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2963
4806
|
workspace: node.workspace,
|
|
2964
4807
|
repoRoot: node.repoRoot,
|
|
2965
4808
|
isLocalWorktree: node.isLocalWorktree,
|
|
2966
4809
|
worktreeBranch: node.worktreeBranch,
|
|
2967
|
-
|
|
4810
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
4811
|
+
daemonId,
|
|
2968
4812
|
machineId: node.machineId,
|
|
4813
|
+
machineStatus: node.machineStatus,
|
|
2969
4814
|
health: 'unknown',
|
|
2970
4815
|
providers: node.providers || [],
|
|
4816
|
+
providerPriority,
|
|
2971
4817
|
activeSessions: [],
|
|
4818
|
+
activeSessionDetails: [],
|
|
4819
|
+
launchReady: false,
|
|
2972
4820
|
};
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
4821
|
+
if (isSelfNode) {
|
|
4822
|
+
status.connection = {
|
|
4823
|
+
perspective: 'selected_coordinator',
|
|
4824
|
+
source: 'mesh_peer_status',
|
|
4825
|
+
state: 'self',
|
|
4826
|
+
transport: 'local',
|
|
4827
|
+
reported: true,
|
|
4828
|
+
reason: 'Selected coordinator daemon',
|
|
4829
|
+
lastStateChangeAt: refreshedAt,
|
|
4830
|
+
};
|
|
4831
|
+
} else if (daemonId) {
|
|
4832
|
+
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
4833
|
+
status.connection = connection ?? {
|
|
4834
|
+
perspective: 'selected_coordinator',
|
|
4835
|
+
source: 'not_reported',
|
|
4836
|
+
state: 'unknown',
|
|
4837
|
+
transport: 'unknown',
|
|
4838
|
+
reported: false,
|
|
4839
|
+
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
4840
|
+
};
|
|
4841
|
+
} else {
|
|
4842
|
+
status.connection = {
|
|
4843
|
+
perspective: 'selected_coordinator',
|
|
4844
|
+
source: 'not_reported',
|
|
4845
|
+
state: 'unknown',
|
|
4846
|
+
transport: 'unknown',
|
|
4847
|
+
reported: false,
|
|
4848
|
+
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
4849
|
+
};
|
|
2980
4850
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
4851
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
4852
|
+
meshId,
|
|
4853
|
+
node,
|
|
4854
|
+
nodeId,
|
|
4855
|
+
liveSessionRecords: liveMeshSessions,
|
|
4856
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4857
|
+
});
|
|
4858
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4859
|
+
meshId,
|
|
4860
|
+
nodeId,
|
|
4861
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
4862
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
4863
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
4864
|
+
status.workspace = workspace || node.workspace;
|
|
4865
|
+
if (matchedLiveSessionRecords.length > 0) {
|
|
4866
|
+
const sessionIds = matchedLiveSessionRecords
|
|
4867
|
+
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
4868
|
+
.filter(Boolean);
|
|
4869
|
+
const providerTypes = matchedLiveSessionRecords
|
|
4870
|
+
.map((record: any) => readStringValue(record?.providerType))
|
|
4871
|
+
.filter(Boolean) as string[];
|
|
4872
|
+
status.activeSessions = sessionIds;
|
|
4873
|
+
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
4874
|
+
if (providerTypes.length > 0) {
|
|
4875
|
+
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
2985
4876
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
4877
|
+
}
|
|
4878
|
+
if (workspace) {
|
|
4879
|
+
if (!fs.existsSync(workspace)) {
|
|
4880
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
4881
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
4882
|
+
let remoteProbeApplied = false;
|
|
4883
|
+
if (inlineTransitGit) {
|
|
4884
|
+
status.git = inlineTransitGit;
|
|
4885
|
+
status.health = inlineTransitGit.isGitRepo
|
|
4886
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
4887
|
+
: 'degraded';
|
|
4888
|
+
const connection = readObjectRecord(status.connection);
|
|
4889
|
+
const connectionState = readStringValue(connection.state);
|
|
4890
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4891
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4892
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4893
|
+
}
|
|
4894
|
+
remoteProbeApplied = true;
|
|
4895
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
4896
|
+
try {
|
|
4897
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
4898
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4899
|
+
daemonId,
|
|
4900
|
+
workspace,
|
|
4901
|
+
timeoutMs: 8000,
|
|
4902
|
+
});
|
|
4903
|
+
if (remoteGit) {
|
|
4904
|
+
status.git = remoteGit;
|
|
4905
|
+
status.health = remoteGit.isGitRepo
|
|
4906
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
4907
|
+
: 'degraded';
|
|
4908
|
+
const connection = readObjectRecord(status.connection);
|
|
4909
|
+
const connectionState = readStringValue(connection.state);
|
|
4910
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4911
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4912
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4913
|
+
}
|
|
4914
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
4915
|
+
remoteProbeApplied = true;
|
|
4916
|
+
}
|
|
4917
|
+
} catch {
|
|
4918
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
4919
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
4920
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
4921
|
+
status.connection = refreshedConnection;
|
|
4922
|
+
try {
|
|
4923
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
4924
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4925
|
+
daemonId,
|
|
4926
|
+
workspace,
|
|
4927
|
+
timeoutMs: 12000,
|
|
4928
|
+
});
|
|
4929
|
+
if (remoteGit) {
|
|
4930
|
+
status.git = remoteGit;
|
|
4931
|
+
status.health = remoteGit.isGitRepo
|
|
4932
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
4933
|
+
: 'degraded';
|
|
4934
|
+
const connection = readObjectRecord(status.connection);
|
|
4935
|
+
const connectionState = readStringValue(connection.state);
|
|
4936
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
4937
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
4938
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
4939
|
+
}
|
|
4940
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
4941
|
+
remoteProbeApplied = true;
|
|
4942
|
+
}
|
|
4943
|
+
} catch {
|
|
4944
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
2994
4948
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
4949
|
+
if (!remoteProbeApplied) {
|
|
4950
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
4951
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
4952
|
+
&& !isSelfNode
|
|
4953
|
+
&& !!daemonId
|
|
4954
|
+
&& (
|
|
4955
|
+
readStringValue(status.machineStatus) === 'online'
|
|
4956
|
+
|| readStringValue(status.health) === 'online'
|
|
4957
|
+
|| connectionState === 'connecting'
|
|
4958
|
+
|| connectionState === 'connected'
|
|
4959
|
+
|| connectionState === 'unknown'
|
|
4960
|
+
);
|
|
4961
|
+
if (pendingPeerGitProbe) {
|
|
4962
|
+
status.gitProbePending = true;
|
|
4963
|
+
status.health = 'unknown';
|
|
4964
|
+
}
|
|
4965
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
4966
|
+
status,
|
|
4967
|
+
node,
|
|
4968
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
4969
|
+
)) {
|
|
4970
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
4971
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
4972
|
+
nodeStatuses.push(status);
|
|
4973
|
+
continue;
|
|
4974
|
+
}
|
|
4975
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
4976
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
4977
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
4978
|
+
nodeStatuses.push(status);
|
|
4979
|
+
continue;
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
} else {
|
|
4983
|
+
try {
|
|
4984
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
4985
|
+
status.git = gitStatus;
|
|
4986
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
4987
|
+
if (gitStatus.isGitRepo) {
|
|
4988
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
4989
|
+
} else {
|
|
4990
|
+
status.health = 'degraded';
|
|
4991
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
4992
|
+
}
|
|
4993
|
+
} catch {
|
|
4994
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
4995
|
+
status.health = 'degraded';
|
|
4996
|
+
}
|
|
2998
4997
|
}
|
|
2999
4998
|
}
|
|
3000
4999
|
} else {
|
|
3001
5000
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
3002
5001
|
}
|
|
5002
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5003
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
3003
5004
|
nodeStatuses.push(status);
|
|
3004
5005
|
}
|
|
3005
5006
|
|
|
3006
|
-
|
|
5007
|
+
const statusResult = {
|
|
3007
5008
|
success: true,
|
|
3008
5009
|
meshId: mesh.id,
|
|
3009
5010
|
meshName: mesh.name,
|
|
3010
5011
|
repoIdentity: mesh.repoIdentity,
|
|
3011
5012
|
defaultBranch: mesh.defaultBranch,
|
|
5013
|
+
refreshedAt,
|
|
5014
|
+
meshHost,
|
|
5015
|
+
sourceOfTruth: {
|
|
5016
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
5017
|
+
? 'coordinator_inline_mesh_cache'
|
|
5018
|
+
: meshRecord?.source === 'local_config'
|
|
5019
|
+
? 'local_mesh_config'
|
|
5020
|
+
: 'inline_bootstrap_snapshot',
|
|
5021
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
5022
|
+
meshHost: {
|
|
5023
|
+
owner: 'mesh_host_daemon',
|
|
5024
|
+
localRole: meshHost.role,
|
|
5025
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
5026
|
+
hostNodeId: meshHost.hostNodeId,
|
|
5027
|
+
hostAddress: meshHost.hostAddress,
|
|
5028
|
+
},
|
|
5029
|
+
...(requireDirectPeerTruth ? {
|
|
5030
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
5031
|
+
directPeerTruth: {
|
|
5032
|
+
required: true,
|
|
5033
|
+
satisfied: directTruthSatisfied,
|
|
5034
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
5035
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
5036
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
5037
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
5038
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
5039
|
+
},
|
|
5040
|
+
} : {}),
|
|
5041
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
5042
|
+
},
|
|
5043
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
3012
5044
|
nodes: nodeStatuses,
|
|
3013
5045
|
queue: { tasks: queue, summary: queueSummary },
|
|
3014
5046
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
3015
5047
|
};
|
|
5048
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, statusResult, refreshReason);
|
|
5049
|
+
logRepoMeshStatusDebug('return_live', {
|
|
5050
|
+
meshId,
|
|
5051
|
+
command: 'mesh_status',
|
|
5052
|
+
refreshRequested,
|
|
5053
|
+
refreshReason,
|
|
5054
|
+
meshSource: meshRecord.source,
|
|
5055
|
+
directTruth,
|
|
5056
|
+
summary: summarizeRepoMeshStatusDebug(rememberedStatus),
|
|
5057
|
+
});
|
|
5058
|
+
return rememberedStatus;
|
|
3016
5059
|
} catch (e: any) {
|
|
3017
5060
|
return { success: false, error: e.message };
|
|
3018
5061
|
}
|
|
@@ -3070,7 +5113,7 @@ export class DaemonCommandRouter {
|
|
|
3070
5113
|
|
|
3071
5114
|
// 3. Kill OS process if requested
|
|
3072
5115
|
if (killProcess) {
|
|
3073
|
-
const running = isIdeRunning(ideType);
|
|
5116
|
+
const running = await isIdeRunning(ideType);
|
|
3074
5117
|
if (running) {
|
|
3075
5118
|
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
|
|
3076
5119
|
const killed = await killIdeProcess(ideType);
|