@adhdev/daemon-core 0.9.82-rc.1 → 0.9.82-rc.100
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 +10 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +10 -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 +6068 -1214
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6033 -1201
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +60 -0
- package/dist/mesh/mesh-events.d.ts +29 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +176 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/repo-mesh-types.d.ts +167 -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 +255 -14
- 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 +28 -10
- package/src/commands/chat-commands.ts +570 -20
- package/src/commands/cli-manager.ts +129 -1
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3095 -406
- package/src/config/chat-history.ts +9 -7
- package/src/config/mesh-config.ts +245 -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 +31 -8
- package/src/mesh/mesh-active-work.ts +255 -0
- package/src/mesh/mesh-events.ts +400 -47
- 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 +356 -0
- package/src/providers/chat-message-normalization.ts +7 -12
- package/src/providers/cli-provider-instance.ts +93 -14
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +182 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
package/src/commands/router.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { getSavedProviderSessions } from '../config/saved-sessions.js';
|
|
|
26
26
|
import { listProviderHistorySessions } from '../config/chat-history.js';
|
|
27
27
|
import { detectIDEs } from '../detection/ide-detector.js';
|
|
28
28
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
29
|
+
import { getGitRepoStatus } from '../git/git-status.js';
|
|
30
|
+
import type { GitSubmoduleStatus } from '../git/git-types.js';
|
|
29
31
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
30
32
|
import { LOG } from '../logging/logger.js';
|
|
31
33
|
import { logCommand } from '../logging/command-log.js';
|
|
@@ -36,13 +38,25 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
|
|
|
36
38
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
37
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
38
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
39
|
-
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents } from '../mesh/mesh-events.js';
|
|
41
|
+
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
42
|
+
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
43
|
+
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
44
|
+
import {
|
|
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';
|
|
40
53
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
41
54
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
42
55
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
56
|
+
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
43
57
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
44
|
-
import { homedir } from 'os';
|
|
45
|
-
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
58
|
+
import { homedir, hostname as osHostname } from 'os';
|
|
59
|
+
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
46
60
|
import * as fs from 'fs';
|
|
47
61
|
|
|
48
62
|
type ReleaseChannel = 'stable' | 'preview';
|
|
@@ -85,6 +99,1062 @@ function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
|
85
99
|
});
|
|
86
100
|
}
|
|
87
101
|
|
|
102
|
+
function readObjectRecord(value: unknown): Record<string, any> {
|
|
103
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
104
|
+
? value as Record<string, any>
|
|
105
|
+
: {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readStringValue(...values: unknown[]): string | undefined {
|
|
109
|
+
for (const value of values) {
|
|
110
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readNumberValue(...values: unknown[]): number | undefined {
|
|
116
|
+
for (const value of values) {
|
|
117
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
123
|
+
for (const value of values) {
|
|
124
|
+
if (typeof value === 'boolean') return value;
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
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 {
|
|
210
|
+
if (!Array.isArray(value)) return undefined;
|
|
211
|
+
const submodules = value
|
|
212
|
+
.map(entry => {
|
|
213
|
+
const submodule = readObjectRecord(entry);
|
|
214
|
+
const path = readStringValue(submodule.path);
|
|
215
|
+
const commit = readStringValue(submodule.commit);
|
|
216
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
217
|
+
?? joinRepoPath(parentRepoRoot, path);
|
|
218
|
+
if (!path || !commit || !repoPath) return null;
|
|
219
|
+
return {
|
|
220
|
+
path,
|
|
221
|
+
commit,
|
|
222
|
+
repoPath,
|
|
223
|
+
dirty: readBooleanValue(submodule.dirty) ?? false,
|
|
224
|
+
outOfSync: readBooleanValue(submodule.outOfSync, submodule.out_of_sync) ?? false,
|
|
225
|
+
lastCheckedAt: readNumberValue(submodule.lastCheckedAt, submodule.last_checked_at) ?? Date.now(),
|
|
226
|
+
...(readStringValue(submodule.error) ? { error: readStringValue(submodule.error) } : {}),
|
|
227
|
+
};
|
|
228
|
+
})
|
|
229
|
+
.filter((entry): entry is GitSubmoduleStatus => entry !== null);
|
|
230
|
+
return submodules.length > 0 ? submodules : undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
|
|
234
|
+
const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
|
|
235
|
+
if (explicit) return explicit;
|
|
236
|
+
const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
|
|
237
|
+
const workspaceName = workspace ? pathBasename(workspace) : undefined;
|
|
238
|
+
const host = readStringValue(node.machineName, node.machine_name, node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
|
|
239
|
+
const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
|
|
240
|
+
const parts = [workspaceName, host, provider].filter(Boolean);
|
|
241
|
+
if (parts.length > 0) return parts.join(' · ');
|
|
242
|
+
return nodeId || 'unidentified mesh node';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normalizeMeshHostname(value: unknown): string | undefined {
|
|
246
|
+
const hostname = readStringValue(value);
|
|
247
|
+
if (!hostname) return undefined;
|
|
248
|
+
return hostname.toLowerCase().replace(/\.$/, '');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
|
|
252
|
+
return readStringValue(
|
|
253
|
+
node.machineId,
|
|
254
|
+
node.machine_id,
|
|
255
|
+
readObjectRecord(node.machine)?.id,
|
|
256
|
+
readObjectRecord(node.machine)?.machineId,
|
|
257
|
+
readObjectRecord(node.lastProbe)?.machineId,
|
|
258
|
+
readObjectRecord(node.last_probe)?.machine_id,
|
|
259
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.id,
|
|
260
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.machineId,
|
|
261
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.id,
|
|
262
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.machine_id,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined {
|
|
267
|
+
return readStringValue(
|
|
268
|
+
node.daemonId,
|
|
269
|
+
node.daemon_id,
|
|
270
|
+
readObjectRecord(node.machine)?.daemonId,
|
|
271
|
+
readObjectRecord(node.machine)?.daemon_id,
|
|
272
|
+
readObjectRecord(node.lastProbe)?.daemonId,
|
|
273
|
+
readObjectRecord(node.last_probe)?.daemon_id,
|
|
274
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemonId,
|
|
275
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemon_id,
|
|
276
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemonId,
|
|
277
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemon_id,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
282
|
+
return readStringValue(
|
|
283
|
+
node.hostname,
|
|
284
|
+
node.host,
|
|
285
|
+
node.machineHostname,
|
|
286
|
+
node.machine_hostname,
|
|
287
|
+
readObjectRecord(node.machine)?.hostname,
|
|
288
|
+
readObjectRecord(node.machine)?.host,
|
|
289
|
+
readObjectRecord(node.lastProbe)?.hostname,
|
|
290
|
+
readObjectRecord(node.last_probe)?.hostname,
|
|
291
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.hostname,
|
|
292
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.hostname,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function readMeshNodeDisplayMachineName(node: Record<string, unknown>): string | undefined {
|
|
297
|
+
return readStringValue(
|
|
298
|
+
node.machineName,
|
|
299
|
+
node.machine_name,
|
|
300
|
+
node.machineLabel,
|
|
301
|
+
node.machine_label,
|
|
302
|
+
node.machineNickname,
|
|
303
|
+
node.machine_nickname,
|
|
304
|
+
node.alias,
|
|
305
|
+
readObjectRecord(node.machine)?.name,
|
|
306
|
+
readObjectRecord(node.machine)?.displayName,
|
|
307
|
+
readObjectRecord(node.machine)?.display_name,
|
|
308
|
+
readObjectRecord(node.lastProbe)?.machineName,
|
|
309
|
+
readObjectRecord(node.last_probe)?.machine_name,
|
|
310
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.name,
|
|
311
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.name,
|
|
312
|
+
readMeshNodeHostname(node),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function compactMeshIdentityEvidence(value: string | undefined): string | undefined {
|
|
317
|
+
if (!value) return undefined;
|
|
318
|
+
return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
322
|
+
localMachineId?: string;
|
|
323
|
+
localDaemonId?: string;
|
|
324
|
+
coordinatorHostname?: string;
|
|
325
|
+
isSelfNode?: boolean;
|
|
326
|
+
}): Record<string, unknown> {
|
|
327
|
+
const machineId = readMeshNodeMachineId(node);
|
|
328
|
+
const daemonId = readMeshNodeDaemonId(node);
|
|
329
|
+
const hostname = readMeshNodeHostname(node);
|
|
330
|
+
const machineName = readMeshNodeDisplayMachineName(node);
|
|
331
|
+
const coordinatorHostname = readStringValue(opts.coordinatorHostname);
|
|
332
|
+
const machineIdMatches = Boolean(opts.localMachineId && machineId && opts.localMachineId === machineId);
|
|
333
|
+
const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && opts.localDaemonId === daemonId);
|
|
334
|
+
const hostnameMatches = Boolean(
|
|
335
|
+
normalizeMeshHostname(hostname)
|
|
336
|
+
&& normalizeMeshHostname(coordinatorHostname)
|
|
337
|
+
&& normalizeMeshHostname(hostname) === normalizeMeshHostname(coordinatorHostname),
|
|
338
|
+
);
|
|
339
|
+
const sameMachine = opts.isSelfNode === true || machineIdMatches || daemonIdMatches || hostnameMatches;
|
|
340
|
+
const evidence: string[] = [];
|
|
341
|
+
for (const [label, value] of [['machineName', machineName], ['hostname', hostname], ['machineId', machineId], ['daemonId', daemonId]] as const) {
|
|
342
|
+
const compact = compactMeshIdentityEvidence(value);
|
|
343
|
+
if (compact) evidence.push(`${label}:${compact}`);
|
|
344
|
+
}
|
|
345
|
+
const locality = sameMachine ? 'same_machine' : (evidence.length > 0 ? 'remote_known' : 'remote_or_unknown');
|
|
346
|
+
const localityReason = sameMachine
|
|
347
|
+
? (machineIdMatches ? 'matched coordinator machine id'
|
|
348
|
+
: daemonIdMatches ? 'matched coordinator daemon id'
|
|
349
|
+
: hostnameMatches ? 'matched coordinator hostname'
|
|
350
|
+
: 'selected coordinator node')
|
|
351
|
+
: evidence.length > 0
|
|
352
|
+
? `known remote/other machine identity; no local coordinator match (${evidence.join(', ')})`
|
|
353
|
+
: 'no useful machine identity evidence available';
|
|
354
|
+
return {
|
|
355
|
+
daemonId,
|
|
356
|
+
machineId,
|
|
357
|
+
hostname,
|
|
358
|
+
machineName,
|
|
359
|
+
displayName: machineName || hostname || daemonId || machineId,
|
|
360
|
+
coordinatorHostname,
|
|
361
|
+
sameMachine,
|
|
362
|
+
locality,
|
|
363
|
+
localityReason,
|
|
364
|
+
identityEvidence: evidence,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function normalizeInlineMeshGitStatus(
|
|
369
|
+
status: Record<string, unknown>,
|
|
370
|
+
node: any,
|
|
371
|
+
options?: { lastCheckedAt?: number },
|
|
372
|
+
): Record<string, unknown> | undefined {
|
|
373
|
+
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
374
|
+
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
375
|
+
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
376
|
+
? status.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
|
|
377
|
+
: [];
|
|
378
|
+
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
379
|
+
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
380
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
381
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
382
|
+
return {
|
|
383
|
+
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
384
|
+
repoRoot: repoRoot ?? null,
|
|
385
|
+
isGitRepo,
|
|
386
|
+
branch: readStringValue(status.branch) ?? null,
|
|
387
|
+
headCommit: readStringValue(status.headCommit) ?? null,
|
|
388
|
+
headMessage: readStringValue(status.headMessage) ?? null,
|
|
389
|
+
upstream: readStringValue(status.upstream) ?? null,
|
|
390
|
+
upstreamStatus: readStringValue(status.upstreamStatus, status.upstream_status)
|
|
391
|
+
?? (readStringValue(status.upstream) ? 'unchecked' : 'no_upstream'),
|
|
392
|
+
upstreamFetchedAt: readNumberValue(status.upstreamFetchedAt, status.upstream_fetched_at),
|
|
393
|
+
upstreamFetchError: readStringValue(status.upstreamFetchError, status.upstream_fetch_error),
|
|
394
|
+
ahead: readNumberValue(status.ahead) ?? 0,
|
|
395
|
+
behind: readNumberValue(status.behind) ?? 0,
|
|
396
|
+
staged: readNumberValue(status.staged) ?? 0,
|
|
397
|
+
modified: readNumberValue(status.modified) ?? 0,
|
|
398
|
+
untracked: readNumberValue(status.untracked) ?? 0,
|
|
399
|
+
deleted: readNumberValue(status.deleted) ?? 0,
|
|
400
|
+
renamed: readNumberValue(status.renamed) ?? 0,
|
|
401
|
+
hasConflicts,
|
|
402
|
+
conflictFiles,
|
|
403
|
+
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
404
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
405
|
+
...(submodules ? { submodules } : {}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
|
|
410
|
+
if (!git) return Number.NEGATIVE_INFINITY;
|
|
411
|
+
let score = 0;
|
|
412
|
+
if (readBooleanValue(git.isGitRepo) === true) score += 50;
|
|
413
|
+
if (readBooleanValue(git.isGitRepo) === false) score -= 10;
|
|
414
|
+
if (readStringValue(git.branch)) score += 20;
|
|
415
|
+
if (readStringValue(git.headCommit)) score += 20;
|
|
416
|
+
if (readStringValue(git.upstream)) score += 10;
|
|
417
|
+
if (readStringValue(git.upstreamStatus)) score += 5;
|
|
418
|
+
if (readNumberValue(git.ahead) !== undefined) score += 2;
|
|
419
|
+
if (readNumberValue(git.behind) !== undefined) score += 2;
|
|
420
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
|
|
421
|
+
if (readStringValue(git.error)) score -= 20;
|
|
422
|
+
return score;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
426
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
427
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
428
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
429
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
430
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
431
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
432
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
433
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
434
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
435
|
+
const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
|
|
436
|
+
let best: { git: Record<string, unknown>; score: number } | null = null;
|
|
437
|
+
for (const status of candidates) {
|
|
438
|
+
const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
439
|
+
if (!normalized) continue;
|
|
440
|
+
const score = scoreInlineMeshGitStatus(normalized);
|
|
441
|
+
if (!best || score > best.score) best = { git: normalized, score };
|
|
442
|
+
}
|
|
443
|
+
return best?.git;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
|
|
447
|
+
if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
|
|
448
|
+
return snapshot.nodes.some((node: any) => {
|
|
449
|
+
if (node?.gitProbePending !== true) return false;
|
|
450
|
+
const git = readObjectRecord(node?.git);
|
|
451
|
+
return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
456
|
+
const source = readStringValue(connection.source);
|
|
457
|
+
const transport = readStringValue(connection.transport);
|
|
458
|
+
return {
|
|
459
|
+
...connection,
|
|
460
|
+
perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
|
|
461
|
+
source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
|
|
462
|
+
state: 'connected',
|
|
463
|
+
transport: transport && transport !== 'unknown' ? transport : 'direct',
|
|
464
|
+
reported: true,
|
|
465
|
+
reason: 'Live peer git snapshot reported by the selected coordinator.',
|
|
466
|
+
lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function recordInlineMeshDirectGitTruth(
|
|
471
|
+
node: any,
|
|
472
|
+
git: Record<string, unknown>,
|
|
473
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
474
|
+
): void {
|
|
475
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
476
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
477
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
478
|
+
const nextGit: Record<string, unknown> = {
|
|
479
|
+
...git,
|
|
480
|
+
lastCheckedAt: checkedAt,
|
|
481
|
+
};
|
|
482
|
+
node.lastGit = {
|
|
483
|
+
source,
|
|
484
|
+
checkedAt,
|
|
485
|
+
status: nextGit,
|
|
486
|
+
};
|
|
487
|
+
node.last_git = node.lastGit;
|
|
488
|
+
node.machineStatus = 'online';
|
|
489
|
+
node.updatedAt = updatedAt;
|
|
490
|
+
node.lastSeenAt = updatedAt;
|
|
491
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
492
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
496
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
497
|
+
if (liveGit) return liveGit;
|
|
498
|
+
|
|
499
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
500
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
501
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
502
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
506
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
507
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
508
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
509
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
510
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
511
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
512
|
+
const branch = readStringValue(cachedGit.branch);
|
|
513
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
514
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
518
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
519
|
+
const {
|
|
520
|
+
cachedStatus,
|
|
521
|
+
lastGit: _lastGit,
|
|
522
|
+
last_git: _lastGitLegacy,
|
|
523
|
+
lastProbe: _lastProbe,
|
|
524
|
+
last_probe: _lastProbeLegacy,
|
|
525
|
+
error: _error,
|
|
526
|
+
health: _health,
|
|
527
|
+
machineStatus: _machineStatus,
|
|
528
|
+
lastSeenAt: _lastSeenAt,
|
|
529
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
530
|
+
updatedAt: _updatedAt,
|
|
531
|
+
updated_at: _updatedAtLegacy,
|
|
532
|
+
activeSession: _activeSession,
|
|
533
|
+
active_session: _activeSessionLegacy,
|
|
534
|
+
activeSessionId: _activeSessionId,
|
|
535
|
+
active_session_id: _activeSessionIdLegacy,
|
|
536
|
+
sessionId: _sessionId,
|
|
537
|
+
session_id: _sessionIdLegacy,
|
|
538
|
+
providerType: _providerType,
|
|
539
|
+
provider_type: _providerTypeLegacy,
|
|
540
|
+
...rest
|
|
541
|
+
} = node as Record<string, unknown>;
|
|
542
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
543
|
+
return { ...rest, cachedStatus };
|
|
544
|
+
}
|
|
545
|
+
return rest;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
549
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
550
|
+
return 'cachedStatus' in node
|
|
551
|
+
|| 'lastGit' in node
|
|
552
|
+
|| 'last_git' in node
|
|
553
|
+
|| 'lastProbe' in node
|
|
554
|
+
|| 'last_probe' in node
|
|
555
|
+
|| 'error' in node
|
|
556
|
+
|| 'health' in node
|
|
557
|
+
|| 'machineStatus' in node
|
|
558
|
+
|| 'lastSeenAt' in node
|
|
559
|
+
|| 'last_seen_at' in node
|
|
560
|
+
|| 'updatedAt' in node
|
|
561
|
+
|| 'updated_at' in node
|
|
562
|
+
|| 'activeSession' in node
|
|
563
|
+
|| 'active_session' in node
|
|
564
|
+
|| 'activeSessionId' in node
|
|
565
|
+
|| 'active_session_id' in node
|
|
566
|
+
|| 'sessionId' in node
|
|
567
|
+
|| 'session_id' in node
|
|
568
|
+
|| 'providerType' in node
|
|
569
|
+
|| 'provider_type' in node;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
573
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
574
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
575
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function readInlineMeshNodeId(node: any): string {
|
|
579
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
583
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
584
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
585
|
+
let changed = false;
|
|
586
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
587
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
588
|
+
changed = true;
|
|
589
|
+
return stripInlineMeshTransientNodeState(node);
|
|
590
|
+
});
|
|
591
|
+
if (!changed) return inlineMesh;
|
|
592
|
+
return {
|
|
593
|
+
...inlineMesh,
|
|
594
|
+
nodes,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
599
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
600
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
601
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
602
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
603
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
604
|
+
|
|
605
|
+
const cachedUpdatedAt = Date.parse(readStringValue(cached.updatedAt, cached.updated_at) || '');
|
|
606
|
+
const incomingUpdatedAt = Date.parse(readStringValue(incoming.updatedAt, incoming.updated_at) || '');
|
|
607
|
+
const preserveCachedMembership = Number.isFinite(cachedUpdatedAt)
|
|
608
|
+
&& (!Number.isFinite(incomingUpdatedAt) || cachedUpdatedAt > incomingUpdatedAt);
|
|
609
|
+
|
|
610
|
+
const cachedById = new Map<string, any>();
|
|
611
|
+
for (const node of cachedNodes) {
|
|
612
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
613
|
+
if (nodeId) cachedById.set(nodeId, node);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const nodes = incomingNodes.map((incomingNode: any) => {
|
|
617
|
+
const nodeId = readInlineMeshNodeId(incomingNode);
|
|
618
|
+
const cachedNode = nodeId ? cachedById.get(nodeId) : undefined;
|
|
619
|
+
if (!cachedNode && preserveCachedMembership) return null;
|
|
620
|
+
if (!cachedNode) return incomingNode;
|
|
621
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
622
|
+
return { ...cachedNode, ...incomingNode };
|
|
623
|
+
}
|
|
624
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
625
|
+
}).filter(Boolean);
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
...cached,
|
|
629
|
+
...incoming,
|
|
630
|
+
nodes,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
635
|
+
return countGitWorktreeChanges(git) > 0;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function countGitWorktreeChanges(git: Record<string, unknown> | null | undefined): number {
|
|
639
|
+
if (!git) return 0;
|
|
640
|
+
return Number(git.staged || 0)
|
|
641
|
+
+ Number(git.modified || 0)
|
|
642
|
+
+ Number(git.untracked || 0)
|
|
643
|
+
+ Number(git.deleted || 0)
|
|
644
|
+
+ Number(git.renamed || 0);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
648
|
+
const submodules = Array.isArray(git?.submodules) ? git.submodules : [];
|
|
649
|
+
let dirty = false;
|
|
650
|
+
let outOfSync = false;
|
|
651
|
+
for (const entry of submodules) {
|
|
652
|
+
const submodule = readObjectRecord(entry);
|
|
653
|
+
if (readBooleanValue(submodule.dirty) === true) dirty = true;
|
|
654
|
+
if (readBooleanValue(submodule.outOfSync) === true || !!readStringValue(submodule.error)) outOfSync = true;
|
|
655
|
+
}
|
|
656
|
+
return { dirty, outOfSync };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undefined): 'online' | 'dirty' | 'degraded' {
|
|
660
|
+
if (!git || readBooleanValue(git.isGitRepo) === false) return 'degraded';
|
|
661
|
+
const branch = readStringValue(git.branch);
|
|
662
|
+
if (!branch) return 'degraded';
|
|
663
|
+
const submoduleDrift = getGitSubmoduleDriftState(git);
|
|
664
|
+
if (submoduleDrift.outOfSync) return 'degraded';
|
|
665
|
+
if (submoduleDrift.dirty || hasGitWorktreeChanges(git)) return 'dirty';
|
|
666
|
+
return 'online';
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function readMeshNodeLabel(status: Record<string, unknown>, node: any): string {
|
|
670
|
+
return readStringValue(status.nodeId, node?.id, node?.nodeId) ?? 'unknown';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function buildInlineMeshBranchConvergence(args: {
|
|
674
|
+
mesh: any;
|
|
675
|
+
node: any;
|
|
676
|
+
status: Record<string, unknown>;
|
|
677
|
+
}): Record<string, unknown> {
|
|
678
|
+
const git = readObjectRecord(args.status.git);
|
|
679
|
+
const nodeLabel = readMeshNodeLabel(args.status, args.node);
|
|
680
|
+
const defaultBranch = readStringValue(args.mesh?.defaultBranch) ?? 'main';
|
|
681
|
+
const branch = readStringValue(git.branch, args.node?.worktreeBranch) ?? null;
|
|
682
|
+
const upstream = readStringValue(git.upstream) ?? null;
|
|
683
|
+
const upstreamStatus = readStringValue(git.upstreamStatus, git.upstream_status)
|
|
684
|
+
?? (upstream ? 'unchecked' : 'no_upstream');
|
|
685
|
+
const ahead = readNumberValue(git.ahead) ?? 0;
|
|
686
|
+
const behind = readNumberValue(git.behind) ?? 0;
|
|
687
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
688
|
+
const hasConflicts = readBooleanValue(git.hasConflicts)
|
|
689
|
+
?? (Array.isArray(git.conflictFiles) && git.conflictFiles.length > 0);
|
|
690
|
+
const base = {
|
|
691
|
+
defaultBranch,
|
|
692
|
+
branch,
|
|
693
|
+
upstream,
|
|
694
|
+
upstreamStatus,
|
|
695
|
+
ahead,
|
|
696
|
+
behind,
|
|
697
|
+
isWorktree: args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true,
|
|
698
|
+
isDefaultBranch: branch === defaultBranch,
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
if (readBooleanValue(git.isGitRepo) !== true) {
|
|
702
|
+
return {
|
|
703
|
+
...base,
|
|
704
|
+
status: 'blocked_review',
|
|
705
|
+
needsConvergence: true,
|
|
706
|
+
reason: 'git_status_unavailable',
|
|
707
|
+
nextStep: `Resolve git status for node '${nodeLabel}' before marking the task complete.`,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (!branch) {
|
|
712
|
+
return {
|
|
713
|
+
...base,
|
|
714
|
+
status: 'blocked_review',
|
|
715
|
+
needsConvergence: true,
|
|
716
|
+
reason: 'branch_unknown',
|
|
717
|
+
nextStep: `Inspect node '${nodeLabel}' git branch before deciding whether it is merged to ${defaultBranch}.`,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (hasConflicts || uncommittedChanges > 0) {
|
|
722
|
+
return {
|
|
723
|
+
...base,
|
|
724
|
+
status: 'not_mergeable',
|
|
725
|
+
needsConvergence: true,
|
|
726
|
+
reason: hasConflicts ? 'conflicts_present' : 'dirty_workspace',
|
|
727
|
+
nextStep: `Commit, checkpoint, or resolve node '${nodeLabel}' before any main convergence step.`,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (branch === defaultBranch) {
|
|
732
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
733
|
+
return {
|
|
734
|
+
...base,
|
|
735
|
+
status: 'blocked_review',
|
|
736
|
+
needsConvergence: true,
|
|
737
|
+
reason: 'default_branch_upstream_unverified',
|
|
738
|
+
nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${nodeLabel}'.`,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (ahead > 0 || behind > 0) {
|
|
742
|
+
return {
|
|
743
|
+
...base,
|
|
744
|
+
status: 'blocked_review',
|
|
745
|
+
needsConvergence: true,
|
|
746
|
+
reason: 'default_branch_not_even_with_upstream',
|
|
747
|
+
nextStep: `Bring ${defaultBranch} even with its upstream before declaring convergence complete.`,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
...base,
|
|
752
|
+
status: 'merged_to_main',
|
|
753
|
+
needsConvergence: false,
|
|
754
|
+
reason: 'clean_default_branch',
|
|
755
|
+
nextStep: null,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true) {
|
|
760
|
+
return {
|
|
761
|
+
...base,
|
|
762
|
+
status: 'cleanup_candidate',
|
|
763
|
+
needsConvergence: true,
|
|
764
|
+
reason: 'clean_non_default_worktree_branch',
|
|
765
|
+
nextStep: `Run mesh_refine_node(node_id: "${nodeLabel}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
770
|
+
return {
|
|
771
|
+
...base,
|
|
772
|
+
status: 'blocked_review',
|
|
773
|
+
needsConvergence: true,
|
|
774
|
+
reason: 'feature_branch_upstream_unverified',
|
|
775
|
+
nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (!upstream || ahead > 0 || behind > 0) {
|
|
780
|
+
return {
|
|
781
|
+
...base,
|
|
782
|
+
status: 'blocked_review',
|
|
783
|
+
needsConvergence: true,
|
|
784
|
+
reason: !upstream ? 'feature_branch_missing_upstream' : 'feature_branch_not_even_with_upstream',
|
|
785
|
+
nextStep: `Push or reconcile branch '${branch}', then merge it into ${defaultBranch} or mark it not_mergeable with a reason.`,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
...base,
|
|
791
|
+
status: 'pushed_feature_branch_needs_merge',
|
|
792
|
+
needsConvergence: true,
|
|
793
|
+
reason: 'clean_non_default_branch',
|
|
794
|
+
nextStep: `Review and merge branch '${branch}' into ${defaultBranch}; do not report the task as fully complete while it remains off main.`,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
799
|
+
const git = readObjectRecord(status.git);
|
|
800
|
+
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
801
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
802
|
+
status.isDirty = uncommittedChanges > 0;
|
|
803
|
+
status.uncommittedChanges = uncommittedChanges;
|
|
804
|
+
status.branchConvergence = buildInlineMeshBranchConvergence({ mesh, node, status });
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
808
|
+
const followUps = nodes
|
|
809
|
+
.filter(node => readObjectRecord(node.branchConvergence).needsConvergence === true)
|
|
810
|
+
.map(node => {
|
|
811
|
+
const convergence = readObjectRecord(node.branchConvergence);
|
|
812
|
+
return {
|
|
813
|
+
nodeId: node.nodeId,
|
|
814
|
+
workspace: node.workspace,
|
|
815
|
+
branch: convergence.branch,
|
|
816
|
+
status: convergence.status,
|
|
817
|
+
reason: convergence.reason,
|
|
818
|
+
nextStep: convergence.nextStep,
|
|
819
|
+
};
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
return {
|
|
823
|
+
needsFollowUp: followUps.length > 0,
|
|
824
|
+
unresolvedCount: followUps.length,
|
|
825
|
+
requiredFinalStates: ['merged_to_main', 'pushed_feature_branch_needs_merge', 'blocked_review', 'cleanup_candidate', 'not_mergeable'],
|
|
826
|
+
followUps,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
831
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
832
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
833
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
834
|
+
? activeSession
|
|
835
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
836
|
+
const sessionId = readStringValue(fallbackSession.id, fallbackSession.sessionId, fallbackSession.session_id, node?.activeSessionId, node?.active_session_id, node?.sessionId, node?.session_id);
|
|
837
|
+
return sessionId ? [sessionId] : [];
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
841
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
842
|
+
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
843
|
+
const fallbackSession = Object.keys(activeSession).length
|
|
844
|
+
? activeSession
|
|
845
|
+
: readObjectRecord(node?.activeSession ?? node?.active_session);
|
|
846
|
+
const sessionId = readStringValue(
|
|
847
|
+
fallbackSession.id,
|
|
848
|
+
fallbackSession.sessionId,
|
|
849
|
+
fallbackSession.session_id,
|
|
850
|
+
node?.activeSessionId,
|
|
851
|
+
node?.active_session_id,
|
|
852
|
+
node?.sessionId,
|
|
853
|
+
node?.session_id,
|
|
854
|
+
);
|
|
855
|
+
if (!sessionId) return [];
|
|
856
|
+
return [{
|
|
857
|
+
sessionId,
|
|
858
|
+
providerType: readStringValue(
|
|
859
|
+
fallbackSession.providerType,
|
|
860
|
+
fallbackSession.provider_type,
|
|
861
|
+
fallbackSession.cliType,
|
|
862
|
+
fallbackSession.cli_type,
|
|
863
|
+
fallbackSession.provider,
|
|
864
|
+
node?.providerType,
|
|
865
|
+
node?.provider_type,
|
|
866
|
+
),
|
|
867
|
+
state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
|
|
868
|
+
lifecycle: readStringValue(fallbackSession.lifecycle),
|
|
869
|
+
title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
|
|
870
|
+
workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
|
|
871
|
+
lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
|
|
872
|
+
recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
|
|
873
|
+
isCached: true,
|
|
874
|
+
}];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function readLiveMeshSessionState(record: any): string | undefined {
|
|
878
|
+
return readStringValue(
|
|
879
|
+
record?.meta?.sessionStatus,
|
|
880
|
+
record?.meta?.status,
|
|
881
|
+
record?.meta?.providerStatus,
|
|
882
|
+
record?.status,
|
|
883
|
+
record?.state,
|
|
884
|
+
record?.lifecycle,
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function toIsoTimestamp(value: unknown): string | null {
|
|
889
|
+
if (typeof value === 'number' && Number.isFinite(value)) return new Date(value).toISOString();
|
|
890
|
+
const stringValue = readStringValue(value);
|
|
891
|
+
return stringValue || null;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
895
|
+
const connection = readObjectRecord(status.connection);
|
|
896
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
897
|
+
const git = readObjectRecord(status.git);
|
|
898
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
899
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
900
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
901
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function finalizeMeshNodeStatus(args: {
|
|
906
|
+
status: Record<string, unknown>;
|
|
907
|
+
node: any;
|
|
908
|
+
daemonId?: string;
|
|
909
|
+
isSelfNode: boolean;
|
|
910
|
+
}): void {
|
|
911
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
912
|
+
if (!readStringValue(status.machineStatus)) {
|
|
913
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
914
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
915
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
916
|
+
}
|
|
917
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
918
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
919
|
+
status.launchReady = !!daemonId && (
|
|
920
|
+
readStringValue(status.machineStatus) === 'online'
|
|
921
|
+
|| connectionState === 'connected'
|
|
922
|
+
|| isSelfNode
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
927
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
928
|
+
daemonId: string;
|
|
929
|
+
workspace: string;
|
|
930
|
+
timeoutMs: number;
|
|
931
|
+
}): Promise<Record<string, unknown> | null> {
|
|
932
|
+
if (!args.dispatchMeshCommand) return null;
|
|
933
|
+
const remoteResult = await Promise.race([
|
|
934
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
|
|
935
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
936
|
+
]) as any;
|
|
937
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
938
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
939
|
+
? remoteGit as Record<string, unknown>
|
|
940
|
+
: null;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
944
|
+
mesh: any;
|
|
945
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
946
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
947
|
+
statusInstanceId?: string;
|
|
948
|
+
localMachineId?: string;
|
|
949
|
+
}): Promise<{
|
|
950
|
+
directEvidenceCount: number;
|
|
951
|
+
localConfirmedCount: number;
|
|
952
|
+
peerAttemptedCount: number;
|
|
953
|
+
peerConfirmedCount: number;
|
|
954
|
+
unavailableNodeIds: string[];
|
|
955
|
+
}> {
|
|
956
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
957
|
+
if (!nodes.length) {
|
|
958
|
+
return {
|
|
959
|
+
directEvidenceCount: 0,
|
|
960
|
+
localConfirmedCount: 0,
|
|
961
|
+
peerAttemptedCount: 0,
|
|
962
|
+
peerConfirmedCount: 0,
|
|
963
|
+
unavailableNodeIds: [],
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
968
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
969
|
+
nodes[0]?.id,
|
|
970
|
+
nodes[0]?.nodeId,
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
let localConfirmedCount = 0;
|
|
974
|
+
let peerAttemptedCount = 0;
|
|
975
|
+
let peerConfirmedCount = 0;
|
|
976
|
+
const unavailableNodeIds: string[] = [];
|
|
977
|
+
|
|
978
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
979
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
980
|
+
const workspace = readStringValue(node?.workspace);
|
|
981
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
982
|
+
const isSelfNode = Boolean(
|
|
983
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
984
|
+
) || Boolean(
|
|
985
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
986
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
987
|
+
|
|
988
|
+
if (!workspace) {
|
|
989
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (fs.existsSync(workspace)) {
|
|
994
|
+
try {
|
|
995
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
996
|
+
if (localGit?.isGitRepo) {
|
|
997
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
998
|
+
localConfirmedCount += 1;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
} catch {
|
|
1002
|
+
// Fall through to remote classification.
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
1007
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
peerAttemptedCount += 1;
|
|
1012
|
+
try {
|
|
1013
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
1014
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1015
|
+
daemonId,
|
|
1016
|
+
workspace,
|
|
1017
|
+
timeoutMs: 8_000,
|
|
1018
|
+
});
|
|
1019
|
+
if (remoteGit) {
|
|
1020
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1021
|
+
peerConfirmedCount += 1;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
} catch {
|
|
1025
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
unavailableNodeIds.push(nodeId);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return {
|
|
1032
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
1033
|
+
localConfirmedCount,
|
|
1034
|
+
peerAttemptedCount,
|
|
1035
|
+
peerConfirmedCount,
|
|
1036
|
+
unavailableNodeIds,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
1041
|
+
return {
|
|
1042
|
+
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
1043
|
+
providerType: readStringValue(record?.providerType),
|
|
1044
|
+
state: readLiveMeshSessionState(record),
|
|
1045
|
+
lifecycle: readStringValue(record?.lifecycle),
|
|
1046
|
+
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
1047
|
+
recoveryState: readStringValue(record?.meta?.runtimeRecoveryState) ?? null,
|
|
1048
|
+
workspace: readStringValue(record?.workspace) ?? null,
|
|
1049
|
+
title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
|
|
1050
|
+
lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
|
|
1051
|
+
isCached: false,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string): boolean {
|
|
1056
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1057
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
1058
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1059
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
1063
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
1064
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
1065
|
+
|
|
1066
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1067
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
1068
|
+
|
|
1069
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
1073
|
+
meshId: string;
|
|
1074
|
+
nodeId: string;
|
|
1075
|
+
liveSessionRecords: any[];
|
|
1076
|
+
allowCoordinatorSession?: boolean;
|
|
1077
|
+
}): string {
|
|
1078
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
1079
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
1080
|
+
&& readStringValue(record?.workspace)
|
|
1081
|
+
));
|
|
1082
|
+
if (directNodeWorkspace) {
|
|
1083
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (args.allowCoordinatorSession) {
|
|
1087
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
1088
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
1089
|
+
&& readStringValue(record?.workspace)
|
|
1090
|
+
));
|
|
1091
|
+
if (coordinatorWorkspace) {
|
|
1092
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return '';
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function collectLiveMeshSessionRecords(args: {
|
|
1100
|
+
meshId: string;
|
|
1101
|
+
node: any;
|
|
1102
|
+
nodeId: string;
|
|
1103
|
+
liveSessionRecords: any[];
|
|
1104
|
+
allowCoordinatorSession?: boolean;
|
|
1105
|
+
}): any[] {
|
|
1106
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
1107
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
1108
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)) return true;
|
|
1109
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
if (args.allowCoordinatorSession) {
|
|
1113
|
+
for (const record of args.liveSessionRecords) {
|
|
1114
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
1115
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
1116
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
1117
|
+
matches.push(record);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return matches;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function applyCachedInlineMeshNodeStatus(
|
|
1125
|
+
status: Record<string, unknown>,
|
|
1126
|
+
node: any,
|
|
1127
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
1128
|
+
): boolean {
|
|
1129
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
1130
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
1131
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
1132
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
1133
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
1134
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
1135
|
+
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
1136
|
+
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
1137
|
+
const activeSessions = readCachedInlineMeshActiveSessions(node);
|
|
1138
|
+
const activeSessionDetails = readCachedInlineMeshActiveSessionDetails(node);
|
|
1139
|
+
if (!git && !error && !health && !machineStatus && !lastSeenAt && !updatedAt && activeSessions.length === 0) return false;
|
|
1140
|
+
if (git) status.git = git;
|
|
1141
|
+
if (error) status.error = error;
|
|
1142
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
1143
|
+
if (lastSeenAt) status.lastSeenAt = lastSeenAt;
|
|
1144
|
+
if (updatedAt) status.updatedAt = updatedAt;
|
|
1145
|
+
if (activeSessions.length > 0) status.activeSessions = activeSessions;
|
|
1146
|
+
if (activeSessionDetails.length > 0) status.activeSessionDetails = activeSessionDetails;
|
|
1147
|
+
if (health) {
|
|
1148
|
+
status.health = health;
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
if (git) {
|
|
1152
|
+
status.health = deriveMeshNodeHealthFromGit(git);
|
|
1153
|
+
return true;
|
|
1154
|
+
}
|
|
1155
|
+
return activeSessions.length > 0 || !!machineStatus || !!lastSeenAt || !!updatedAt;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
88
1158
|
async function resolveProviderTypeFromPriority(args: {
|
|
89
1159
|
nodeId: string;
|
|
90
1160
|
providerPriority: string[];
|
|
@@ -117,29 +1187,114 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
117
1187
|
}
|
|
118
1188
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
119
1189
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
120
|
-
type MeshRefineValidationCommand =
|
|
121
|
-
command: string;
|
|
122
|
-
args: string[];
|
|
123
|
-
displayCommand: string;
|
|
124
|
-
category: string;
|
|
125
|
-
source: string;
|
|
126
|
-
};
|
|
1190
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
127
1191
|
|
|
128
1192
|
type MeshRefineValidationSummary = {
|
|
129
1193
|
status: MeshRefineValidationStatus;
|
|
130
1194
|
required: true;
|
|
131
1195
|
commandsRun: Array<Record<string, unknown>>;
|
|
1196
|
+
bootstrapCommandsRun: Array<Record<string, unknown>>;
|
|
132
1197
|
rejectedCommands: Array<Record<string, unknown>>;
|
|
133
1198
|
skippedReason?: string;
|
|
1199
|
+
failureKind?: string;
|
|
1200
|
+
failureCode?: string;
|
|
134
1201
|
timeoutMs: number;
|
|
135
1202
|
outputLimitBytes: number;
|
|
1203
|
+
configSource?: string;
|
|
1204
|
+
configSourceType?: string;
|
|
1205
|
+
suggestions?: unknown[];
|
|
1206
|
+
suggestedConfig?: unknown;
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
1210
|
+
|
|
1211
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
1212
|
+
status: MeshRefineStageStatus;
|
|
1213
|
+
equivalent: boolean;
|
|
1214
|
+
baseHead: string;
|
|
1215
|
+
branchHead: string;
|
|
1216
|
+
mergeBase?: string;
|
|
1217
|
+
mergedTree?: string;
|
|
1218
|
+
expectedPatchId?: string;
|
|
1219
|
+
actualPatchId?: string;
|
|
1220
|
+
durationMs: number;
|
|
1221
|
+
error?: string;
|
|
1222
|
+
stdout?: string;
|
|
1223
|
+
stderr?: string;
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
type MeshRefineSubmoduleReachabilityEntry = {
|
|
1227
|
+
path: string;
|
|
1228
|
+
commit: string;
|
|
1229
|
+
reachable: boolean;
|
|
1230
|
+
publishRequired?: boolean;
|
|
1231
|
+
autoPublishAllowed?: boolean;
|
|
1232
|
+
autoPublishAttempted?: boolean;
|
|
1233
|
+
autoPublishSucceeded?: boolean;
|
|
1234
|
+
autoPublishVerified?: boolean;
|
|
1235
|
+
autoPublishRefspec?: string;
|
|
1236
|
+
autoPublishSkippedReason?: string;
|
|
1237
|
+
importedFromWorktree?: boolean;
|
|
1238
|
+
checkedLocal?: boolean;
|
|
1239
|
+
localReachable?: boolean;
|
|
1240
|
+
remote?: string;
|
|
1241
|
+
remoteUrl?: string;
|
|
1242
|
+
remoteReachable?: boolean;
|
|
1243
|
+
remoteMainBranch?: string;
|
|
1244
|
+
remoteMainReachable?: boolean;
|
|
1245
|
+
fetchedFromOrigin?: boolean;
|
|
1246
|
+
error?: string;
|
|
1247
|
+
publishStdout?: string;
|
|
1248
|
+
publishStderr?: string;
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
type MeshRefineSubmoduleReachabilitySummary = {
|
|
1252
|
+
status: MeshRefineStageStatus;
|
|
1253
|
+
checked: number;
|
|
1254
|
+
unreachable: MeshRefineSubmoduleReachabilityEntry[];
|
|
1255
|
+
entries: MeshRefineSubmoduleReachabilityEntry[];
|
|
1256
|
+
durationMs: number;
|
|
1257
|
+
autoPublishAllowed?: boolean;
|
|
1258
|
+
autoPublishPolicySource?: string;
|
|
1259
|
+
error?: string;
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1263
|
+
|
|
1264
|
+
type MeshRefineJobHandle = {
|
|
1265
|
+
success: true;
|
|
1266
|
+
async: true;
|
|
1267
|
+
status: MeshRefineAsyncJobStatus;
|
|
1268
|
+
jobId: string;
|
|
1269
|
+
interactionId: string;
|
|
1270
|
+
meshId: string;
|
|
1271
|
+
nodeId: string;
|
|
1272
|
+
targetNodeId: string;
|
|
1273
|
+
targetDaemonId?: string;
|
|
1274
|
+
workspace?: string;
|
|
1275
|
+
startedAt: string;
|
|
1276
|
+
completedAt?: string;
|
|
1277
|
+
duplicate?: boolean;
|
|
1278
|
+
retryOfJobId?: string;
|
|
1279
|
+
eventDelivery: {
|
|
1280
|
+
pendingEvents: true;
|
|
1281
|
+
ledger: true;
|
|
1282
|
+
};
|
|
1283
|
+
evidence: {
|
|
1284
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
1285
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
1286
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
1287
|
+
};
|
|
136
1288
|
};
|
|
137
1289
|
|
|
1290
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
1291
|
+
|
|
138
1292
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
139
1293
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
140
1294
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
141
1295
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
142
1296
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
1297
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
143
1298
|
|
|
144
1299
|
function truncateValidationOutput(value: unknown): string {
|
|
145
1300
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -147,171 +1302,336 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
147
1302
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
148
1303
|
}
|
|
149
1304
|
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const trimmed = command.trim();
|
|
164
|
-
if (!trimmed) return null;
|
|
165
|
-
// Fail closed: the gate never hands shell syntax to a shell. Package-manager
|
|
166
|
-
// scripts are invoked via execFile(binary, args), and metacharacters/quotes are
|
|
167
|
-
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
168
|
-
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
169
|
-
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
170
|
-
if (!tokens.length) return null;
|
|
171
|
-
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
172
|
-
return tokens;
|
|
1305
|
+
function recordMeshRefineStage(
|
|
1306
|
+
stages: Array<Record<string, unknown>>,
|
|
1307
|
+
stage: string,
|
|
1308
|
+
status: MeshRefineStageStatus,
|
|
1309
|
+
startedAt: number,
|
|
1310
|
+
details?: Record<string, unknown>,
|
|
1311
|
+
): void {
|
|
1312
|
+
stages.push({
|
|
1313
|
+
stage,
|
|
1314
|
+
status,
|
|
1315
|
+
durationMs: Date.now() - startedAt,
|
|
1316
|
+
...(details || {}),
|
|
1317
|
+
});
|
|
173
1318
|
}
|
|
174
1319
|
|
|
175
|
-
function
|
|
176
|
-
|
|
1320
|
+
function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
|
|
1321
|
+
const refs = entries
|
|
1322
|
+
.map(entry => `${entry.path}@${entry.commit}`)
|
|
1323
|
+
.join(', ');
|
|
1324
|
+
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
|
|
177
1325
|
}
|
|
178
1326
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
scripts: Record<string, string>,
|
|
183
|
-
source: string,
|
|
184
|
-
): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
|
|
185
|
-
const tokens = tokenizeValidationCommand(rawCommand);
|
|
186
|
-
if (!tokens) {
|
|
187
|
-
return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
|
|
1327
|
+
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1328
|
+
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1329
|
+
return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
|
|
188
1330
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let command = binary;
|
|
193
|
-
let args: string[] = [];
|
|
194
|
-
|
|
195
|
-
if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
|
|
196
|
-
scriptName = third;
|
|
197
|
-
args = ['run', scriptName, ...rest];
|
|
198
|
-
} else if (binary === 'npm' && second === 'test' && !third) {
|
|
199
|
-
scriptName = 'test';
|
|
200
|
-
args = ['test'];
|
|
201
|
-
} else if (binary === 'yarn' && second === 'run' && third) {
|
|
202
|
-
scriptName = third;
|
|
203
|
-
args = ['run', scriptName, ...rest];
|
|
204
|
-
} else if (binary === 'yarn' && second && !third) {
|
|
205
|
-
scriptName = second;
|
|
206
|
-
args = [scriptName];
|
|
207
|
-
} else {
|
|
208
|
-
return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
212
|
-
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
|
|
213
|
-
}
|
|
214
|
-
if (!scriptMatchesValidationCategory(scriptName, category)) {
|
|
215
|
-
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
|
|
1331
|
+
const loaded = loadMeshRefineConfig(mesh, workspace);
|
|
1332
|
+
if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1333
|
+
return { enabled: true, source: loaded.path || loaded.source };
|
|
216
1334
|
}
|
|
1335
|
+
return { enabled: false };
|
|
1336
|
+
}
|
|
217
1337
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
1338
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
1339
|
+
const { execFileSync } = await import('node:child_process');
|
|
1340
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
1341
|
+
cwd,
|
|
1342
|
+
encoding: 'utf8',
|
|
1343
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1344
|
+
});
|
|
1345
|
+
if (!diff.trim()) return '';
|
|
1346
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
1347
|
+
cwd,
|
|
1348
|
+
input: diff,
|
|
1349
|
+
encoding: 'utf8',
|
|
1350
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1351
|
+
}).trim();
|
|
1352
|
+
return patchId.split(/\s+/)[0] || '';
|
|
227
1353
|
}
|
|
228
1354
|
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
1355
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
1356
|
+
repoRoot: string,
|
|
1357
|
+
baseHead: string,
|
|
1358
|
+
branchHead: string,
|
|
1359
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
1360
|
+
const startedAt = Date.now();
|
|
1361
|
+
try {
|
|
1362
|
+
const { execFileSync } = await import('node:child_process');
|
|
1363
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
1364
|
+
cwd: repoRoot,
|
|
1365
|
+
encoding: 'utf8',
|
|
1366
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1367
|
+
});
|
|
1368
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
1369
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
1370
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
1371
|
+
if (!mergeBase || !mergedTree) {
|
|
1372
|
+
return {
|
|
1373
|
+
status: 'failed',
|
|
1374
|
+
equivalent: false,
|
|
1375
|
+
baseHead,
|
|
1376
|
+
branchHead,
|
|
1377
|
+
mergeBase: mergeBase || undefined,
|
|
1378
|
+
mergedTree: mergedTree || undefined,
|
|
1379
|
+
durationMs: Date.now() - startedAt,
|
|
1380
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
1381
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
1382
|
+
};
|
|
243
1383
|
}
|
|
1384
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
1385
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
1386
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
1387
|
+
return {
|
|
1388
|
+
status: equivalent ? 'passed' : 'failed',
|
|
1389
|
+
equivalent,
|
|
1390
|
+
baseHead,
|
|
1391
|
+
branchHead,
|
|
1392
|
+
mergeBase,
|
|
1393
|
+
mergedTree,
|
|
1394
|
+
expectedPatchId,
|
|
1395
|
+
actualPatchId,
|
|
1396
|
+
durationMs: Date.now() - startedAt,
|
|
1397
|
+
};
|
|
1398
|
+
} catch (e: any) {
|
|
1399
|
+
return {
|
|
1400
|
+
status: 'failed',
|
|
1401
|
+
equivalent: false,
|
|
1402
|
+
baseHead,
|
|
1403
|
+
branchHead,
|
|
1404
|
+
durationMs: Date.now() - startedAt,
|
|
1405
|
+
error: e?.message || String(e),
|
|
1406
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1407
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1408
|
+
};
|
|
244
1409
|
}
|
|
245
|
-
return candidates.sort((a, b) => {
|
|
246
|
-
const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
|
|
247
|
-
return rank(a.confidence) - rank(b.confidence);
|
|
248
|
-
});
|
|
249
1410
|
}
|
|
250
1411
|
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
1412
|
+
async function runMeshRefineSubmoduleReachabilityGate(
|
|
1413
|
+
repoRoot: string,
|
|
1414
|
+
mergedTree: string,
|
|
1415
|
+
options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
|
|
1416
|
+
): Promise<MeshRefineSubmoduleReachabilitySummary> {
|
|
1417
|
+
const startedAt = Date.now();
|
|
1418
|
+
const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
|
|
1419
|
+
try {
|
|
1420
|
+
const { execFile } = await import('node:child_process');
|
|
1421
|
+
const { promisify } = await import('node:util');
|
|
1422
|
+
const execFileAsync = promisify(execFile);
|
|
1423
|
+
const runGit = async (cwd: string, args: string[]): Promise<string> => {
|
|
1424
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
1425
|
+
cwd,
|
|
1426
|
+
encoding: 'utf8',
|
|
1427
|
+
timeout: 30_000,
|
|
1428
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1429
|
+
windowsHide: true,
|
|
1430
|
+
});
|
|
1431
|
+
return String(stdout || '');
|
|
1432
|
+
};
|
|
1433
|
+
const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
|
|
1434
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1435
|
+
await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
|
|
1436
|
+
};
|
|
1437
|
+
const publishCommitToRemoteMain = async (submodulePath: string, commit: string, branch = 'main'): Promise<{ stdout: string; stderr: string; refspec: string }> => {
|
|
1438
|
+
const refspec = `${commit}:refs/heads/${branch}`;
|
|
1439
|
+
const { stdout, stderr } = await execFileAsync('git', ['push', 'origin', refspec], {
|
|
1440
|
+
cwd: submodulePath,
|
|
1441
|
+
encoding: 'utf8',
|
|
1442
|
+
timeout: 30_000,
|
|
1443
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1444
|
+
windowsHide: true,
|
|
1445
|
+
});
|
|
1446
|
+
return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
|
|
1447
|
+
};
|
|
1448
|
+
const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
|
|
1449
|
+
if (!fs.existsSync(worktreeSubmodulePath)) return false;
|
|
1450
|
+
try {
|
|
1451
|
+
await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1452
|
+
} catch {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
|
|
1456
|
+
await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1457
|
+
return true;
|
|
1458
|
+
};
|
|
268
1459
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1460
|
+
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1461
|
+
const gitlinks = treeOutput
|
|
1462
|
+
.split('\0')
|
|
1463
|
+
.filter(Boolean)
|
|
1464
|
+
.map(record => {
|
|
1465
|
+
const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
1466
|
+
return match ? { commit: match[1], path: match[2] } : null;
|
|
1467
|
+
})
|
|
1468
|
+
.filter((entry): entry is { commit: string; path: string } => !!entry);
|
|
1469
|
+
|
|
1470
|
+
for (const gitlink of gitlinks) {
|
|
1471
|
+
const submodulePath = pathResolve(repoRoot, gitlink.path);
|
|
1472
|
+
const entry: MeshRefineSubmoduleReachabilityEntry = {
|
|
1473
|
+
path: gitlink.path,
|
|
1474
|
+
commit: gitlink.commit,
|
|
1475
|
+
reachable: false,
|
|
1476
|
+
};
|
|
1477
|
+
try {
|
|
1478
|
+
if (!fs.existsSync(submodulePath)) {
|
|
1479
|
+
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1480
|
+
entry.publishRequired = true;
|
|
1481
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1482
|
+
entry.autoPublishAllowed = true;
|
|
1483
|
+
entry.autoPublishAttempted = false;
|
|
1484
|
+
entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
|
|
1485
|
+
}
|
|
1486
|
+
entries.push(entry);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
entry.checkedLocal = true;
|
|
1491
|
+
try {
|
|
1492
|
+
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1493
|
+
entry.localReachable = true;
|
|
1494
|
+
} catch {
|
|
1495
|
+
entry.localReachable = false;
|
|
1496
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
|
|
1497
|
+
try {
|
|
1498
|
+
const imported = await importCommitFromWorktreeSubmodule(
|
|
1499
|
+
submodulePath,
|
|
1500
|
+
pathResolve(options.worktreeRoot, gitlink.path),
|
|
1501
|
+
gitlink.commit,
|
|
1502
|
+
);
|
|
1503
|
+
if (imported) {
|
|
1504
|
+
entry.localReachable = true;
|
|
1505
|
+
entry.importedFromWorktree = true;
|
|
1506
|
+
}
|
|
1507
|
+
} catch (importError: any) {
|
|
1508
|
+
entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
// Probe the submodule remote before allowing cleanup/completion.
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
try {
|
|
1515
|
+
entry.remote = 'origin';
|
|
1516
|
+
let remoteUrl = '';
|
|
1517
|
+
try {
|
|
1518
|
+
remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
|
|
1519
|
+
if (!remoteUrl) throw new Error('origin remote has no URL');
|
|
1520
|
+
entry.remoteUrl = remoteUrl;
|
|
1521
|
+
} catch {
|
|
1522
|
+
entry.error = 'Submodule remote reachability check failed: no configured origin remote';
|
|
1523
|
+
entry.publishRequired = true;
|
|
1524
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1525
|
+
entry.autoPublishAllowed = true;
|
|
1526
|
+
entry.autoPublishAttempted = false;
|
|
1527
|
+
entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
|
|
1528
|
+
}
|
|
1529
|
+
entries.push(entry);
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
entry.remoteMainBranch = 'main';
|
|
1533
|
+
try {
|
|
1534
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1535
|
+
entry.fetchedFromOrigin = true;
|
|
1536
|
+
entry.remoteReachable = true;
|
|
1537
|
+
entry.remoteMainReachable = true;
|
|
1538
|
+
entry.reachable = true;
|
|
1539
|
+
} catch (e: any) {
|
|
1540
|
+
entry.remoteReachable = false;
|
|
1541
|
+
entry.remoteMainReachable = false;
|
|
1542
|
+
entry.publishRequired = true;
|
|
1543
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1544
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1545
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
|
|
1546
|
+
entry.autoPublishAllowed = true;
|
|
1547
|
+
entry.autoPublishAttempted = true;
|
|
1548
|
+
try {
|
|
1549
|
+
const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, 'main');
|
|
1550
|
+
entry.autoPublishRefspec = publish.refspec;
|
|
1551
|
+
entry.publishStdout = truncateValidationOutput(publish.stdout);
|
|
1552
|
+
entry.publishStderr = truncateValidationOutput(publish.stderr);
|
|
1553
|
+
entry.autoPublishSucceeded = true;
|
|
1554
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1555
|
+
entry.fetchedFromOrigin = true;
|
|
1556
|
+
entry.remoteReachable = true;
|
|
1557
|
+
entry.remoteMainReachable = true;
|
|
1558
|
+
entry.autoPublishVerified = true;
|
|
1559
|
+
entry.publishRequired = false;
|
|
1560
|
+
entry.reachable = true;
|
|
1561
|
+
entry.error = undefined;
|
|
1562
|
+
} catch (publishError: any) {
|
|
1563
|
+
entry.autoPublishSucceeded = false;
|
|
1564
|
+
entry.autoPublishVerified = false;
|
|
1565
|
+
const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
|
|
1566
|
+
entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
|
|
1567
|
+
}
|
|
1568
|
+
} else if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1569
|
+
entry.autoPublishAllowed = true;
|
|
1570
|
+
entry.autoPublishAttempted = false;
|
|
1571
|
+
entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
|
|
1572
|
+
|| 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
} catch (e: any) {
|
|
1576
|
+
entry.remoteReachable = false;
|
|
1577
|
+
entry.remoteMainReachable = false;
|
|
1578
|
+
entry.publishRequired = true;
|
|
1579
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1580
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1581
|
+
}
|
|
1582
|
+
} catch (e: any) {
|
|
1583
|
+
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
1584
|
+
entry.publishRequired = true;
|
|
1585
|
+
}
|
|
1586
|
+
entries.push(entry);
|
|
302
1587
|
}
|
|
1588
|
+
|
|
1589
|
+
const unreachable = entries.filter(entry => !entry.reachable);
|
|
1590
|
+
return {
|
|
1591
|
+
status: unreachable.length ? 'failed' : 'passed',
|
|
1592
|
+
checked: entries.length,
|
|
1593
|
+
unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
|
|
1594
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
|
|
1595
|
+
durationMs: Date.now() - startedAt,
|
|
1596
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1597
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1598
|
+
};
|
|
1599
|
+
} catch (e: any) {
|
|
1600
|
+
const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
|
|
1601
|
+
return {
|
|
1602
|
+
status: 'failed',
|
|
1603
|
+
checked: entries.length,
|
|
1604
|
+
unreachable,
|
|
1605
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
|
|
1606
|
+
durationMs: Date.now() - startedAt,
|
|
1607
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1608
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1609
|
+
error: truncateValidationOutput(e?.message || String(e)),
|
|
1610
|
+
};
|
|
303
1611
|
}
|
|
1612
|
+
}
|
|
304
1613
|
|
|
1614
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1615
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1616
|
+
const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
|
|
1617
|
+
displayCommand: command.displayCommand,
|
|
1618
|
+
category: command.category,
|
|
1619
|
+
source: command.source,
|
|
1620
|
+
cwd: command.cwd,
|
|
1621
|
+
timeoutMs: command.timeoutMs,
|
|
1622
|
+
});
|
|
305
1623
|
return {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
1624
|
+
source: plan.source,
|
|
1625
|
+
sourceType: plan.sourceType,
|
|
1626
|
+
bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
|
|
1627
|
+
commands: plan.commands.map(mapCommand),
|
|
1628
|
+
unavailableReason: plan.unavailableReason,
|
|
1629
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1630
|
+
suggestions: plan.suggestions,
|
|
1631
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1632
|
+
note: plan.sourceType === 'unavailable'
|
|
1633
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1634
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
315
1635
|
};
|
|
316
1636
|
}
|
|
317
1637
|
|
|
@@ -319,60 +1639,119 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
319
1639
|
const { execFile } = await import('node:child_process');
|
|
320
1640
|
const { promisify } = await import('node:util');
|
|
321
1641
|
const execFileAsync = promisify(execFile);
|
|
322
|
-
const selection =
|
|
1642
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
323
1643
|
const summary: MeshRefineValidationSummary = {
|
|
324
1644
|
status: 'skipped',
|
|
325
1645
|
required: true,
|
|
326
1646
|
commandsRun: [],
|
|
1647
|
+
bootstrapCommandsRun: [],
|
|
327
1648
|
rejectedCommands: selection.rejectedCommands,
|
|
328
1649
|
skippedReason: undefined,
|
|
329
1650
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
330
1651
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1652
|
+
configSource: selection.source,
|
|
1653
|
+
configSourceType: selection.sourceType,
|
|
1654
|
+
suggestions: selection.suggestions,
|
|
1655
|
+
suggestedConfig: selection.suggestedConfig,
|
|
331
1656
|
};
|
|
332
1657
|
|
|
333
1658
|
if (!selection.commands.length) {
|
|
334
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1659
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
335
1660
|
return summary;
|
|
336
1661
|
}
|
|
337
1662
|
|
|
338
|
-
|
|
1663
|
+
const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
|
|
1664
|
+
command: candidate.command,
|
|
1665
|
+
args: candidate.args,
|
|
1666
|
+
displayCommand: candidate.displayCommand,
|
|
1667
|
+
category: candidate.category,
|
|
1668
|
+
source: candidate.source,
|
|
1669
|
+
cwd,
|
|
1670
|
+
passed,
|
|
1671
|
+
durationMs: Date.now() - startedAt,
|
|
1672
|
+
stdout: truncateValidationOutput(result?.stdout),
|
|
1673
|
+
stderr: truncateValidationOutput(result?.stderr || result?.message),
|
|
1674
|
+
...extras,
|
|
1675
|
+
});
|
|
1676
|
+
const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
|
|
1677
|
+
const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
|
|
1678
|
+
return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
|
|
1679
|
+
&& candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
|
|
1680
|
+
};
|
|
1681
|
+
const dependenciesLikelyMissing = (cwd: string): boolean => {
|
|
1682
|
+
if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
|
|
1683
|
+
if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
|
|
1684
|
+
return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
|
|
1685
|
+
.some(lock => fs.existsSync(pathJoin(cwd, lock)));
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
for (const candidate of selection.bootstrapCommands) {
|
|
339
1689
|
const startedAt = Date.now();
|
|
1690
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1691
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
340
1692
|
try {
|
|
341
1693
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
342
|
-
cwd
|
|
1694
|
+
cwd,
|
|
343
1695
|
encoding: 'utf8',
|
|
344
|
-
timeout
|
|
1696
|
+
timeout,
|
|
345
1697
|
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
346
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
347
|
-
});
|
|
348
|
-
summary.commandsRun.push({
|
|
349
|
-
command: candidate.command,
|
|
350
|
-
args: candidate.args,
|
|
351
|
-
displayCommand: candidate.displayCommand,
|
|
352
|
-
category: candidate.category,
|
|
353
|
-
source: candidate.source,
|
|
354
|
-
passed: true,
|
|
355
|
-
exitCode: 0,
|
|
356
|
-
durationMs: Date.now() - startedAt,
|
|
357
|
-
stdout: truncateValidationOutput(result.stdout),
|
|
358
|
-
stderr: truncateValidationOutput(result.stderr),
|
|
1698
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
359
1699
|
});
|
|
1700
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
360
1701
|
} catch (error: any) {
|
|
361
|
-
summary.
|
|
362
|
-
command: candidate.command,
|
|
363
|
-
args: candidate.args,
|
|
364
|
-
displayCommand: candidate.displayCommand,
|
|
365
|
-
category: candidate.category,
|
|
366
|
-
source: candidate.source,
|
|
367
|
-
passed: false,
|
|
1702
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
368
1703
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
369
1704
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
370
1705
|
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
1706
|
+
failureKind: 'dependency_bootstrap_failed',
|
|
1707
|
+
}));
|
|
1708
|
+
summary.status = 'failed';
|
|
1709
|
+
summary.failureKind = 'dependency_bootstrap_failed';
|
|
1710
|
+
summary.failureCode = 'dependency_bootstrap_failed';
|
|
1711
|
+
return summary;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
for (const candidate of selection.commands) {
|
|
1716
|
+
const startedAt = Date.now();
|
|
1717
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1718
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1719
|
+
if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
|
|
1720
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
|
|
1721
|
+
stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
|
|
1722
|
+
}, false, {
|
|
1723
|
+
exitCode: null,
|
|
1724
|
+
skipped: true,
|
|
1725
|
+
failureKind: 'missing_dependencies',
|
|
1726
|
+
}));
|
|
1727
|
+
summary.status = 'failed';
|
|
1728
|
+
summary.failureKind = 'missing_dependencies';
|
|
1729
|
+
summary.failureCode = 'missing_dependencies';
|
|
1730
|
+
return summary;
|
|
1731
|
+
}
|
|
1732
|
+
try {
|
|
1733
|
+
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
1734
|
+
cwd,
|
|
1735
|
+
encoding: 'utf8',
|
|
1736
|
+
timeout,
|
|
1737
|
+
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1738
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
374
1739
|
});
|
|
1740
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1741
|
+
} catch (error: any) {
|
|
1742
|
+
const stderr = truncateValidationOutput(error?.stderr || error?.message);
|
|
1743
|
+
const missingDependencyFailure = /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
|
|
1744
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
1745
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
1746
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
1747
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
1748
|
+
...(missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
|
|
1749
|
+
}));
|
|
375
1750
|
summary.status = 'failed';
|
|
1751
|
+
if (missingDependencyFailure) {
|
|
1752
|
+
summary.failureKind = 'missing_dependencies';
|
|
1753
|
+
summary.failureCode = 'missing_dependencies';
|
|
1754
|
+
}
|
|
376
1755
|
return summary;
|
|
377
1756
|
}
|
|
378
1757
|
}
|
|
@@ -502,6 +1881,10 @@ export interface CommandRouterDeps {
|
|
|
502
1881
|
statusVersion?: string;
|
|
503
1882
|
/** Session host control plane */
|
|
504
1883
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
1884
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
1885
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
1886
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
1887
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
505
1888
|
}
|
|
506
1889
|
|
|
507
1890
|
export interface CommandRouterResult {
|
|
@@ -613,33 +1996,266 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
613
1996
|
};
|
|
614
1997
|
}
|
|
615
1998
|
|
|
1999
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
2000
|
+
const raw = hostAddress.trim();
|
|
2001
|
+
if (!raw) throw new Error('hostAddress required');
|
|
2002
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
2003
|
+
url.pathname = '/api/v1/command';
|
|
2004
|
+
url.search = '';
|
|
2005
|
+
url.hash = '';
|
|
2006
|
+
return url.toString();
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
2010
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
2011
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
2012
|
+
? args.memberNode as Record<string, any>
|
|
2013
|
+
: null;
|
|
2014
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
2015
|
+
? (requestedNodeId
|
|
2016
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
2017
|
+
: mesh.nodes[0])
|
|
2018
|
+
: null;
|
|
2019
|
+
const source = explicit || configured;
|
|
2020
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
2021
|
+
? source.workspace.trim()
|
|
2022
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
2023
|
+
? args.workspace.trim()
|
|
2024
|
+
: process.cwd();
|
|
2025
|
+
if (!workspace) return null;
|
|
2026
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
2027
|
+
? source.id.trim()
|
|
2028
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
2029
|
+
? source.nodeId.trim()
|
|
2030
|
+
: undefined;
|
|
2031
|
+
return {
|
|
2032
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
2033
|
+
workspace,
|
|
2034
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
2035
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
2036
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
2037
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
2038
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
2039
|
+
role: 'member',
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
|
|
616
2043
|
export class DaemonCommandRouter {
|
|
617
2044
|
private deps: CommandRouterDeps;
|
|
618
2045
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
619
2046
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
620
2047
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
621
2048
|
private inlineMeshCache = new Map<string, any>();
|
|
2049
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
2050
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
|
|
2051
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
2052
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
2053
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
2054
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
622
2055
|
|
|
623
2056
|
constructor(deps: CommandRouterDeps) {
|
|
624
2057
|
this.deps = deps;
|
|
625
2058
|
}
|
|
626
2059
|
|
|
2060
|
+
private cloneJsonValue<T>(value: T): T {
|
|
2061
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
2062
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
2066
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
2067
|
+
const inlineNodesById = new Map<string, any>();
|
|
2068
|
+
for (const node of mesh.nodes) {
|
|
2069
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
2070
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
2071
|
+
}
|
|
2072
|
+
if (!inlineNodesById.size) return snapshot;
|
|
2073
|
+
|
|
2074
|
+
let changed = false;
|
|
2075
|
+
const unavailableNodeIds = new Set<string>();
|
|
2076
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
2077
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
2078
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
2079
|
+
const nodeId = readStringValue(entry);
|
|
2080
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
2084
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
2085
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
2086
|
+
if (!inlineNode) return statusNode;
|
|
2087
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
2088
|
+
if (!liveGit) return statusNode;
|
|
2089
|
+
const nextStatus = { ...statusNode };
|
|
2090
|
+
nextStatus.git = liveGit;
|
|
2091
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
2092
|
+
applyInlineMeshBranchConvergence(mesh, inlineNode, nextStatus);
|
|
2093
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
2094
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
2095
|
+
const connectionState = readStringValue(connection.state);
|
|
2096
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
2097
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
2098
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
2099
|
+
}
|
|
2100
|
+
delete nextStatus.gitProbePending;
|
|
2101
|
+
const error = readStringValue(nextStatus.error);
|
|
2102
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
2103
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
2104
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
2105
|
+
changed = true;
|
|
2106
|
+
return nextStatus;
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
const aggregateDirectTruthSatisfied = sourceOfTruth.coordinatorOwnsLiveTruth === true
|
|
2110
|
+
|| directPeerTruth.satisfied === true;
|
|
2111
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied)) return snapshot;
|
|
2112
|
+
const nextSourceOfTruth = {
|
|
2113
|
+
...sourceOfTruth,
|
|
2114
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
2115
|
+
directPeerTruth: {
|
|
2116
|
+
...directPeerTruth,
|
|
2117
|
+
satisfied: options?.requireDirectPeerTruth === true
|
|
2118
|
+
? aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0
|
|
2119
|
+
: directPeerTruth.satisfied,
|
|
2120
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
2121
|
+
},
|
|
2122
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
2123
|
+
coordinatorOwnsLiveTruth: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0,
|
|
2124
|
+
currentStatus: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
2125
|
+
} : {}),
|
|
2126
|
+
} : {}),
|
|
2127
|
+
};
|
|
2128
|
+
return {
|
|
2129
|
+
...snapshot,
|
|
2130
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied ? {
|
|
2131
|
+
success: false,
|
|
2132
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
2133
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
2134
|
+
} : {}),
|
|
2135
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
2136
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodes),
|
|
2137
|
+
nodes,
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
2142
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
2143
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
2144
|
+
if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
|
|
2145
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
2146
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
2147
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
2148
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
2149
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
2150
|
+
? snapshot.sourceOfTruth
|
|
2151
|
+
: {};
|
|
2152
|
+
snapshot.sourceOfTruth = {
|
|
2153
|
+
...sourceOfTruth,
|
|
2154
|
+
aggregateSnapshot: {
|
|
2155
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
2156
|
+
? sourceOfTruth.aggregateSnapshot
|
|
2157
|
+
: {}),
|
|
2158
|
+
owner: 'coordinator_daemon_memory',
|
|
2159
|
+
cached: true,
|
|
2160
|
+
source: 'memory',
|
|
2161
|
+
refreshReason: 'memory_cache_hit',
|
|
2162
|
+
ageMs,
|
|
2163
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
2164
|
+
returnedAt: new Date().toISOString(),
|
|
2165
|
+
},
|
|
2166
|
+
};
|
|
2167
|
+
return snapshot;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
2171
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
2172
|
+
const builtAt = Date.now();
|
|
2173
|
+
const next = this.cloneJsonValue(snapshot);
|
|
2174
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
2175
|
+
? next.sourceOfTruth
|
|
2176
|
+
: {};
|
|
2177
|
+
next.sourceOfTruth = {
|
|
2178
|
+
...sourceOfTruth,
|
|
2179
|
+
aggregateSnapshot: {
|
|
2180
|
+
owner: 'coordinator_daemon_memory',
|
|
2181
|
+
cached: false,
|
|
2182
|
+
source: 'live_refresh',
|
|
2183
|
+
refreshReason,
|
|
2184
|
+
ageMs: 0,
|
|
2185
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
2186
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
2187
|
+
},
|
|
2188
|
+
};
|
|
2189
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
|
|
2190
|
+
return next;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
627
2193
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
628
2194
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
629
|
-
this.
|
|
630
|
-
return inlineMesh as any;
|
|
2195
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
631
2196
|
}
|
|
632
2197
|
return this.inlineMeshCache.get(meshId);
|
|
633
2198
|
}
|
|
634
2199
|
|
|
635
|
-
private
|
|
2200
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
2201
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
2202
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
2203
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
2204
|
+
if (cached) {
|
|
2205
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
2206
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
2207
|
+
return merged;
|
|
2208
|
+
}
|
|
2209
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
2210
|
+
return sanitizedInlineMesh as any;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
private async getMeshForCommand(
|
|
2214
|
+
meshId: string,
|
|
2215
|
+
inlineMesh?: unknown,
|
|
2216
|
+
options?: { preferInline?: boolean },
|
|
2217
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
2218
|
+
const preferInline = options?.preferInline === true;
|
|
2219
|
+
if (preferInline) {
|
|
2220
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
2221
|
+
if (cached) {
|
|
2222
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
2223
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
2224
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
2225
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
2226
|
+
}
|
|
2227
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
2228
|
+
}
|
|
2229
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
2230
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
2231
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
636
2234
|
try {
|
|
637
2235
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
638
2236
|
const mesh = getMesh(meshId);
|
|
639
|
-
if (mesh) return { mesh, inline: false };
|
|
2237
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
640
2238
|
} catch { /* fall through to inline cache */ }
|
|
641
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
642
|
-
|
|
2239
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
2240
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
2241
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
2242
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
2246
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
2251
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
2252
|
+
const mesh = meshRecord?.mesh;
|
|
2253
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2254
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
2255
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
2256
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
2257
|
+
}
|
|
2258
|
+
return null;
|
|
643
2259
|
}
|
|
644
2260
|
|
|
645
2261
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -649,6 +2265,7 @@ export class DaemonCommandRouter {
|
|
|
649
2265
|
else mesh.nodes.push(node);
|
|
650
2266
|
mesh.updatedAt = new Date().toISOString();
|
|
651
2267
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2268
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
652
2269
|
}
|
|
653
2270
|
|
|
654
2271
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -658,6 +2275,7 @@ export class DaemonCommandRouter {
|
|
|
658
2275
|
mesh.nodes.splice(idx, 1);
|
|
659
2276
|
mesh.updatedAt = new Date().toISOString();
|
|
660
2277
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2278
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
661
2279
|
return true;
|
|
662
2280
|
}
|
|
663
2281
|
|
|
@@ -936,6 +2554,7 @@ export class DaemonCommandRouter {
|
|
|
936
2554
|
const deletedSessionIds: string[] = [];
|
|
937
2555
|
const skippedSessionIds: string[] = [];
|
|
938
2556
|
const skippedLiveSessionIds: string[] = [];
|
|
2557
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
939
2558
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
940
2559
|
const recordsRemainSessionIds: string[] = [];
|
|
941
2560
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -970,6 +2589,12 @@ export class DaemonCommandRouter {
|
|
|
970
2589
|
const completed = this.isCompletedHostedSession(record);
|
|
971
2590
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
972
2591
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
2592
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
2593
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
2594
|
+
skippedSessionIds.push(sessionId);
|
|
2595
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
973
2598
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
974
2599
|
skippedSessionIds.push(sessionId);
|
|
975
2600
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1039,6 +2664,7 @@ export class DaemonCommandRouter {
|
|
|
1039
2664
|
deletedSessionIds,
|
|
1040
2665
|
skippedSessionIds,
|
|
1041
2666
|
skippedLiveSessionIds,
|
|
2667
|
+
skippedCoordinatorSessionIds,
|
|
1042
2668
|
...(deleteUnsupported ? {
|
|
1043
2669
|
deleteUnsupported: true,
|
|
1044
2670
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1160,23 +2786,534 @@ export class DaemonCommandRouter {
|
|
|
1160
2786
|
payload: { cmd, source: logSource, success: handlerResult.success, durationMs: Date.now() - cmdStart },
|
|
1161
2787
|
});
|
|
1162
2788
|
|
|
1163
|
-
// 3. Post-chat command callback
|
|
1164
|
-
if (CHAT_COMMANDS.includes(cmd) && this.deps.onPostChatCommand) {
|
|
1165
|
-
this.deps.onPostChatCommand();
|
|
2789
|
+
// 3. Post-chat command callback
|
|
2790
|
+
if (CHAT_COMMANDS.includes(cmd) && this.deps.onPostChatCommand) {
|
|
2791
|
+
this.deps.onPostChatCommand();
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
return handlerResult;
|
|
2795
|
+
} catch (e: any) {
|
|
2796
|
+
logCommand({ ts: new Date().toISOString(), cmd, source: logSource, interactionId, args: normalizedArgs, success: false, error: e.message, durationMs: Date.now() - cmdStart });
|
|
2797
|
+
recordDebugTrace({
|
|
2798
|
+
interactionId,
|
|
2799
|
+
category: 'command',
|
|
2800
|
+
stage: 'failed',
|
|
2801
|
+
level: 'error',
|
|
2802
|
+
payload: { cmd, source: logSource, error: e?.message || String(e), durationMs: Date.now() - cmdStart },
|
|
2803
|
+
});
|
|
2804
|
+
throw e;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
|
|
2809
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
2810
|
+
return `${meshId}:${nodeId}`;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
private buildRefineJobHandle(args: {
|
|
2814
|
+
meshId: string;
|
|
2815
|
+
nodeId: string;
|
|
2816
|
+
node?: any;
|
|
2817
|
+
status?: MeshRefineAsyncJobStatus;
|
|
2818
|
+
startedAt?: string;
|
|
2819
|
+
completedAt?: string;
|
|
2820
|
+
jobId?: string;
|
|
2821
|
+
interactionId?: string;
|
|
2822
|
+
retryOfJobId?: string;
|
|
2823
|
+
}): MeshRefineJobHandle {
|
|
2824
|
+
return {
|
|
2825
|
+
success: true,
|
|
2826
|
+
async: true,
|
|
2827
|
+
status: args.status || 'accepted',
|
|
2828
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
2829
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
2830
|
+
meshId: args.meshId,
|
|
2831
|
+
nodeId: args.nodeId,
|
|
2832
|
+
targetNodeId: args.nodeId,
|
|
2833
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
2834
|
+
workspace: readStringValue(args.node?.workspace),
|
|
2835
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
2836
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
2837
|
+
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
2838
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
2839
|
+
evidence: {
|
|
2840
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
2841
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
2842
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
2843
|
+
},
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
2848
|
+
const metadataEvent = {
|
|
2849
|
+
source: 'refine_mesh_node_async_job',
|
|
2850
|
+
jobId: handle.jobId,
|
|
2851
|
+
interactionId: handle.interactionId,
|
|
2852
|
+
meshId: handle.meshId,
|
|
2853
|
+
nodeId: handle.targetNodeId,
|
|
2854
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2855
|
+
workspace: handle.workspace,
|
|
2856
|
+
status: handle.status,
|
|
2857
|
+
startedAt: handle.startedAt,
|
|
2858
|
+
completedAt: handle.completedAt,
|
|
2859
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2860
|
+
...(result ? { result } : {}),
|
|
2861
|
+
};
|
|
2862
|
+
const eventPayload = {
|
|
2863
|
+
event,
|
|
2864
|
+
meshId: handle.meshId,
|
|
2865
|
+
nodeLabel: handle.targetNodeId,
|
|
2866
|
+
nodeId: handle.targetNodeId,
|
|
2867
|
+
workspace: handle.workspace,
|
|
2868
|
+
metadataEvent,
|
|
2869
|
+
queuedAt: Date.now(),
|
|
2870
|
+
};
|
|
2871
|
+
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
2872
|
+
const forwarded = handleMeshForwardEvent(
|
|
2873
|
+
{ instanceManager: this.deps.instanceManager } as any,
|
|
2874
|
+
{
|
|
2875
|
+
event,
|
|
2876
|
+
meshId: handle.meshId,
|
|
2877
|
+
nodeId: handle.targetNodeId,
|
|
2878
|
+
workspace: handle.workspace,
|
|
2879
|
+
jobId: handle.jobId,
|
|
2880
|
+
interactionId: handle.interactionId,
|
|
2881
|
+
status: handle.status,
|
|
2882
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2883
|
+
startedAt: handle.startedAt,
|
|
2884
|
+
completedAt: handle.completedAt,
|
|
2885
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2886
|
+
...(result ? { result } : {}),
|
|
2887
|
+
},
|
|
2888
|
+
);
|
|
2889
|
+
if (forwarded?.success === true) return;
|
|
2890
|
+
LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
|
|
2891
|
+
}
|
|
2892
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
2896
|
+
try {
|
|
2897
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2898
|
+
appendLedgerEntry(handle.meshId, {
|
|
2899
|
+
kind,
|
|
2900
|
+
nodeId: handle.targetNodeId,
|
|
2901
|
+
payload: {
|
|
2902
|
+
source: 'refine_mesh_node_async_job',
|
|
2903
|
+
refineJob: {
|
|
2904
|
+
jobId: handle.jobId,
|
|
2905
|
+
interactionId: handle.interactionId,
|
|
2906
|
+
status: handle.status,
|
|
2907
|
+
meshId: handle.meshId,
|
|
2908
|
+
nodeId: handle.targetNodeId,
|
|
2909
|
+
targetDaemonId: handle.targetDaemonId,
|
|
2910
|
+
workspace: handle.workspace,
|
|
2911
|
+
startedAt: handle.startedAt,
|
|
2912
|
+
completedAt: handle.completedAt,
|
|
2913
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2914
|
+
},
|
|
2915
|
+
async: true,
|
|
2916
|
+
retryOfJobId: handle.retryOfJobId,
|
|
2917
|
+
...(result ? {
|
|
2918
|
+
success: result.success === true,
|
|
2919
|
+
result,
|
|
2920
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
2921
|
+
} : {}),
|
|
2922
|
+
},
|
|
2923
|
+
});
|
|
2924
|
+
} catch (e: any) {
|
|
2925
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
2930
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
2931
|
+
try {
|
|
2932
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2933
|
+
const mesh = meshRecord?.mesh;
|
|
2934
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2935
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
2936
|
+
|
|
2937
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
2938
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
const sourceNode = node.clonedFromNodeId
|
|
2942
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2943
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2944
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2945
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
2946
|
+
|
|
2947
|
+
const { execFile } = await import('node:child_process');
|
|
2948
|
+
const { promisify } = await import('node:util');
|
|
2949
|
+
const execFileAsync = promisify(execFile);
|
|
2950
|
+
|
|
2951
|
+
const resolveStarted = Date.now();
|
|
2952
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2953
|
+
const branch = branchStdout.trim();
|
|
2954
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
2955
|
+
|
|
2956
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2957
|
+
const baseBranch = baseBranchStdout.trim();
|
|
2958
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2959
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
2960
|
+
const baseHead = baseHeadStdout.trim();
|
|
2961
|
+
const branchHead = branchHeadStdout.trim();
|
|
2962
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
2963
|
+
|
|
2964
|
+
const validationStarted = Date.now();
|
|
2965
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2966
|
+
recordMeshRefineStage(
|
|
2967
|
+
refineStages,
|
|
2968
|
+
'validation',
|
|
2969
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
2970
|
+
validationStarted,
|
|
2971
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
2972
|
+
);
|
|
2973
|
+
if (validationSummary.status === 'failed') {
|
|
2974
|
+
return {
|
|
2975
|
+
success: false,
|
|
2976
|
+
code: validationSummary.failureCode || 'validation_failed',
|
|
2977
|
+
convergenceStatus: 'blocked_review',
|
|
2978
|
+
error: validationSummary.failureCode === 'missing_dependencies'
|
|
2979
|
+
? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
|
|
2980
|
+
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
2981
|
+
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
2982
|
+
: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2983
|
+
branch,
|
|
2984
|
+
into: baseBranch,
|
|
2985
|
+
validationSummary,
|
|
2986
|
+
refineStages,
|
|
2987
|
+
finalBranchConvergenceState: {
|
|
2988
|
+
branch,
|
|
2989
|
+
baseBranch,
|
|
2990
|
+
merged: false,
|
|
2991
|
+
removed: false,
|
|
2992
|
+
validation: 'failed',
|
|
2993
|
+
status: 'blocked_review',
|
|
2994
|
+
},
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
if (validationSummary.status === 'skipped') {
|
|
2998
|
+
return {
|
|
2999
|
+
success: false,
|
|
3000
|
+
code: 'validation_unavailable',
|
|
3001
|
+
convergenceStatus: 'blocked_review',
|
|
3002
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
3003
|
+
branch,
|
|
3004
|
+
into: baseBranch,
|
|
3005
|
+
validationSummary,
|
|
3006
|
+
refineStages,
|
|
3007
|
+
finalBranchConvergenceState: {
|
|
3008
|
+
branch,
|
|
3009
|
+
baseBranch,
|
|
3010
|
+
merged: false,
|
|
3011
|
+
removed: false,
|
|
3012
|
+
validation: 'unavailable',
|
|
3013
|
+
status: 'blocked_review',
|
|
3014
|
+
},
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
const patchEquivalenceStarted = Date.now();
|
|
3019
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3020
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
3021
|
+
equivalent: patchEquivalence.equivalent,
|
|
3022
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
3023
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
3024
|
+
error: patchEquivalence.error,
|
|
3025
|
+
});
|
|
3026
|
+
if (!patchEquivalence.equivalent) {
|
|
3027
|
+
return {
|
|
3028
|
+
success: false,
|
|
3029
|
+
code: 'patch_equivalence_failed',
|
|
3030
|
+
convergenceStatus: 'blocked_review',
|
|
3031
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
3032
|
+
branch,
|
|
3033
|
+
into: baseBranch,
|
|
3034
|
+
validationSummary,
|
|
3035
|
+
patchEquivalence,
|
|
3036
|
+
refineStages,
|
|
3037
|
+
finalBranchConvergenceState: {
|
|
3038
|
+
branch,
|
|
3039
|
+
baseBranch,
|
|
3040
|
+
merged: false,
|
|
3041
|
+
removed: false,
|
|
3042
|
+
validation: 'passed',
|
|
3043
|
+
patchEquivalence: 'failed',
|
|
3044
|
+
status: 'blocked_review',
|
|
3045
|
+
},
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
const submoduleReachabilityStarted = Date.now();
|
|
3050
|
+
const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
|
|
3051
|
+
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
3052
|
+
allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
|
|
3053
|
+
autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
|
|
3054
|
+
worktreeRoot: node.workspace,
|
|
3055
|
+
});
|
|
3056
|
+
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
3057
|
+
checked: submoduleReachability.checked,
|
|
3058
|
+
autoPublishAllowed: submoduleReachability.autoPublishAllowed,
|
|
3059
|
+
autoPublishPolicySource: submoduleReachability.autoPublishPolicySource,
|
|
3060
|
+
autoPublished: submoduleReachability.entries
|
|
3061
|
+
.filter(entry => entry.autoPublishAttempted)
|
|
3062
|
+
.map(entry => ({
|
|
3063
|
+
path: entry.path,
|
|
3064
|
+
commit: entry.commit,
|
|
3065
|
+
remote: entry.remote,
|
|
3066
|
+
remoteUrl: entry.remoteUrl,
|
|
3067
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3068
|
+
refspec: entry.autoPublishRefspec,
|
|
3069
|
+
succeeded: entry.autoPublishSucceeded,
|
|
3070
|
+
verified: entry.autoPublishVerified,
|
|
3071
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3072
|
+
error: entry.error,
|
|
3073
|
+
})),
|
|
3074
|
+
autoPublishSkipped: submoduleReachability.entries
|
|
3075
|
+
.filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
|
|
3076
|
+
.map(entry => ({
|
|
3077
|
+
path: entry.path,
|
|
3078
|
+
commit: entry.commit,
|
|
3079
|
+
remote: entry.remote,
|
|
3080
|
+
remoteUrl: entry.remoteUrl,
|
|
3081
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3082
|
+
reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
|
|
3083
|
+
})),
|
|
3084
|
+
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
3085
|
+
path: entry.path,
|
|
3086
|
+
commit: entry.commit,
|
|
3087
|
+
publishRequired: entry.publishRequired === true,
|
|
3088
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
3089
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
3090
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
3091
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
3092
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3093
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
3094
|
+
remote: entry.remote,
|
|
3095
|
+
remoteUrl: entry.remoteUrl,
|
|
3096
|
+
remoteReachable: entry.remoteReachable,
|
|
3097
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3098
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3099
|
+
error: entry.error,
|
|
3100
|
+
})),
|
|
3101
|
+
error: submoduleReachability.error,
|
|
3102
|
+
});
|
|
3103
|
+
if (submoduleReachability.status === 'failed') {
|
|
3104
|
+
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
3105
|
+
return {
|
|
3106
|
+
success: false,
|
|
3107
|
+
code: 'submodule_reachability_failed',
|
|
3108
|
+
convergenceStatus: 'blocked_review',
|
|
3109
|
+
publishRequired: true,
|
|
3110
|
+
blockedReason: 'submodule_publish_required',
|
|
3111
|
+
error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
|
|
3112
|
+
nextStep,
|
|
3113
|
+
nextSteps: [
|
|
3114
|
+
'Ask the user for explicit approval before pushing or publishing any submodule commit.',
|
|
3115
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
|
|
3116
|
+
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
3117
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
|
|
3118
|
+
],
|
|
3119
|
+
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
3120
|
+
path: entry.path,
|
|
3121
|
+
commit: entry.commit,
|
|
3122
|
+
remote: entry.remote,
|
|
3123
|
+
remoteUrl: entry.remoteUrl,
|
|
3124
|
+
remoteReachable: entry.remoteReachable,
|
|
3125
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3126
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3127
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
3128
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
3129
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
3130
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
3131
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3132
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
3133
|
+
error: entry.error,
|
|
3134
|
+
})),
|
|
3135
|
+
branch,
|
|
3136
|
+
into: baseBranch,
|
|
3137
|
+
validationSummary,
|
|
3138
|
+
patchEquivalence,
|
|
3139
|
+
submoduleReachability,
|
|
3140
|
+
refineStages,
|
|
3141
|
+
finalBranchConvergenceState: {
|
|
3142
|
+
branch,
|
|
3143
|
+
baseBranch,
|
|
3144
|
+
merged: false,
|
|
3145
|
+
removed: false,
|
|
3146
|
+
validation: 'passed',
|
|
3147
|
+
patchEquivalence: 'passed',
|
|
3148
|
+
submoduleReachability: 'failed',
|
|
3149
|
+
status: 'blocked_review',
|
|
3150
|
+
reason: 'submodule_publish_required',
|
|
3151
|
+
nextStep,
|
|
3152
|
+
},
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
3157
|
+
const mergeStarted = Date.now();
|
|
3158
|
+
try {
|
|
3159
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3160
|
+
mergeResult = {
|
|
3161
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
3162
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
3163
|
+
durationMs: Date.now() - mergeStarted,
|
|
3164
|
+
};
|
|
3165
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
3166
|
+
} catch (e: any) {
|
|
3167
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
3168
|
+
error: e?.message || String(e),
|
|
3169
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
3170
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
3171
|
+
});
|
|
3172
|
+
return {
|
|
3173
|
+
success: false,
|
|
3174
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
3175
|
+
validationSummary,
|
|
3176
|
+
patchEquivalence,
|
|
3177
|
+
refineStages,
|
|
3178
|
+
finalBranchConvergenceState: {
|
|
3179
|
+
branch,
|
|
3180
|
+
baseBranch,
|
|
3181
|
+
merged: false,
|
|
3182
|
+
removed: false,
|
|
3183
|
+
validation: 'passed',
|
|
3184
|
+
patchEquivalence: 'passed',
|
|
3185
|
+
status: 'not_mergeable',
|
|
3186
|
+
},
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
const cleanupStarted = Date.now();
|
|
3191
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
3192
|
+
meshId,
|
|
3193
|
+
nodeId,
|
|
3194
|
+
sessionCleanupMode: 'preserve',
|
|
3195
|
+
inlineMesh: args?.inlineMesh,
|
|
3196
|
+
});
|
|
3197
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
3198
|
+
removed: removeResult?.removed,
|
|
3199
|
+
code: removeResult?.code,
|
|
3200
|
+
error: removeResult?.error,
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3203
|
+
let ledgerError: string | undefined;
|
|
3204
|
+
const ledgerStarted = Date.now();
|
|
3205
|
+
try {
|
|
3206
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3207
|
+
appendLedgerEntry(meshId, {
|
|
3208
|
+
kind: 'node_removed',
|
|
3209
|
+
nodeId,
|
|
3210
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability },
|
|
3211
|
+
});
|
|
3212
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
3213
|
+
} catch (e: any) {
|
|
3214
|
+
ledgerError = e?.message || String(e);
|
|
3215
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
const finalBranchConvergenceState = {
|
|
3219
|
+
branch: baseBranch,
|
|
3220
|
+
mergedBranch: branch,
|
|
3221
|
+
baseBranch,
|
|
3222
|
+
merged: true,
|
|
3223
|
+
removed: removeResult?.success !== false,
|
|
3224
|
+
validation: 'passed',
|
|
3225
|
+
patchEquivalence: 'passed',
|
|
3226
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
3227
|
+
};
|
|
3228
|
+
|
|
3229
|
+
if (removeResult?.success === false) {
|
|
3230
|
+
return {
|
|
3231
|
+
success: false,
|
|
3232
|
+
code: 'cleanup_failed',
|
|
3233
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
3234
|
+
merged: true,
|
|
3235
|
+
branch,
|
|
3236
|
+
into: baseBranch,
|
|
3237
|
+
removeResult,
|
|
3238
|
+
validationSummary,
|
|
3239
|
+
patchEquivalence,
|
|
3240
|
+
submoduleReachability,
|
|
3241
|
+
mergeResult,
|
|
3242
|
+
refineStages,
|
|
3243
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3244
|
+
finalBranchConvergenceState,
|
|
3245
|
+
};
|
|
1166
3246
|
}
|
|
1167
3247
|
|
|
1168
|
-
return
|
|
3248
|
+
return {
|
|
3249
|
+
success: true,
|
|
3250
|
+
merged: true,
|
|
3251
|
+
branch,
|
|
3252
|
+
into: baseBranch,
|
|
3253
|
+
removeResult,
|
|
3254
|
+
validationSummary,
|
|
3255
|
+
patchEquivalence,
|
|
3256
|
+
submoduleReachability,
|
|
3257
|
+
mergeResult,
|
|
3258
|
+
refineStages,
|
|
3259
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3260
|
+
finalBranchConvergenceState,
|
|
3261
|
+
};
|
|
1169
3262
|
} catch (e: any) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
3263
|
+
return { success: false, error: e.message, refineStages };
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
3268
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
3269
|
+
let result: Record<string, unknown>;
|
|
3270
|
+
try {
|
|
3271
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
3272
|
+
} catch (e: any) {
|
|
3273
|
+
result = { success: false, error: e?.message || String(e) };
|
|
1179
3274
|
}
|
|
3275
|
+
const completedAt = new Date().toISOString();
|
|
3276
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
3277
|
+
meshId: handle.meshId,
|
|
3278
|
+
nodeId: handle.targetNodeId,
|
|
3279
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
3280
|
+
startedAt: handle.startedAt,
|
|
3281
|
+
completedAt,
|
|
3282
|
+
jobId: handle.jobId,
|
|
3283
|
+
interactionId: handle.interactionId,
|
|
3284
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3285
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
3286
|
+
});
|
|
3287
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
3288
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
3289
|
+
this.runningRefineJobs.delete(key);
|
|
3290
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
3291
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
3292
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
3296
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
3297
|
+
const running = this.runningRefineJobs.get(key);
|
|
3298
|
+
if (running) return { ...running, duplicate: true };
|
|
3299
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
3300
|
+
|
|
3301
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3302
|
+
const mesh = meshRecord?.mesh;
|
|
3303
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3304
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
3305
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
3306
|
+
|
|
3307
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
|
|
3308
|
+
this.runningRefineJobs.set(key, handle);
|
|
3309
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
3310
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
3311
|
+
|
|
3312
|
+
setImmediate(() => {
|
|
3313
|
+
void this.finishMeshRefineJob(handle, args);
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3316
|
+
return handle;
|
|
1180
3317
|
}
|
|
1181
3318
|
|
|
1182
3319
|
// ─── Daemon-level command core ───────────────────
|
|
@@ -1193,7 +3330,8 @@ export class DaemonCommandRouter {
|
|
|
1193
3330
|
}
|
|
1194
3331
|
|
|
1195
3332
|
case 'get_pending_mesh_events': {
|
|
1196
|
-
const
|
|
3333
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3334
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined);
|
|
1197
3335
|
return { success: true, events };
|
|
1198
3336
|
}
|
|
1199
3337
|
|
|
@@ -1798,15 +3936,44 @@ export class DaemonCommandRouter {
|
|
|
1798
3936
|
case 'get_mesh': {
|
|
1799
3937
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1800
3938
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
3939
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3940
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
3941
|
+
|
|
3942
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
3943
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
3944
|
+
mesh: meshRecord.mesh,
|
|
3945
|
+
meshSource: meshRecord.source,
|
|
3946
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
3947
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
3948
|
+
localMachineId: loadConfig().machineId || '',
|
|
3949
|
+
});
|
|
3950
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
3951
|
+
const sourceOfTruth = {
|
|
3952
|
+
membership: meshRecord.source === 'inline_cache'
|
|
3953
|
+
? 'coordinator_inline_mesh_cache'
|
|
3954
|
+
: meshRecord.source === 'local_config'
|
|
3955
|
+
? 'local_mesh_config'
|
|
3956
|
+
: 'inline_bootstrap_snapshot',
|
|
3957
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
3958
|
+
directPeerTruth: {
|
|
3959
|
+
required: requireDirectPeerTruth,
|
|
3960
|
+
satisfied: directTruthSatisfied,
|
|
3961
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
3962
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
3963
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
3964
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
3965
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
3966
|
+
},
|
|
3967
|
+
};
|
|
3968
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
3969
|
+
return {
|
|
3970
|
+
success: false,
|
|
3971
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
3972
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
3973
|
+
sourceOfTruth,
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
1810
3977
|
}
|
|
1811
3978
|
|
|
1812
3979
|
case 'create_mesh': {
|
|
@@ -1817,7 +3984,10 @@ export class DaemonCommandRouter {
|
|
|
1817
3984
|
if (!name) return { success: false, error: 'name required' };
|
|
1818
3985
|
try {
|
|
1819
3986
|
const { createMesh } = await import('../config/mesh-config.js');
|
|
1820
|
-
const
|
|
3987
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
3988
|
+
? args.meshHost
|
|
3989
|
+
: undefined;
|
|
3990
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
1821
3991
|
return { success: true, mesh };
|
|
1822
3992
|
} catch (e: any) {
|
|
1823
3993
|
return { success: false, error: e.message };
|
|
@@ -1834,16 +4004,237 @@ export class DaemonCommandRouter {
|
|
|
1834
4004
|
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
1835
4005
|
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
1836
4006
|
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
4007
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
1837
4008
|
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
1838
4009
|
const mesh = updateMesh(meshId, patch as any);
|
|
1839
4010
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1840
4011
|
this.inlineMeshCache.set(meshId, mesh);
|
|
4012
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
1841
4013
|
return { success: true, mesh };
|
|
1842
4014
|
} catch (e: any) {
|
|
1843
4015
|
return { success: false, error: e.message };
|
|
1844
4016
|
}
|
|
1845
4017
|
}
|
|
1846
4018
|
|
|
4019
|
+
case 'get_mesh_host_pairing': {
|
|
4020
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4021
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4022
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4023
|
+
const mesh = meshRecord?.mesh;
|
|
4024
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4025
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4026
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
4027
|
+
return {
|
|
4028
|
+
success: true,
|
|
4029
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
4030
|
+
meshId,
|
|
4031
|
+
hostAddress: meshHost.hostAddress,
|
|
4032
|
+
meshHost,
|
|
4033
|
+
manualPairing: {
|
|
4034
|
+
status: pairingStatus,
|
|
4035
|
+
joinImplemented: true,
|
|
4036
|
+
protocol: 'standalone_command_direct_v1',
|
|
4037
|
+
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.',
|
|
4038
|
+
},
|
|
4039
|
+
};
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
case 'configure_mesh_host_pairing': {
|
|
4043
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4044
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
4045
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4046
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4047
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
4048
|
+
try {
|
|
4049
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
4050
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
4051
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
4052
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
4053
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
4054
|
+
return {
|
|
4055
|
+
success: true,
|
|
4056
|
+
code: 'mesh_host_pairing_pending',
|
|
4057
|
+
meshId,
|
|
4058
|
+
hostAddress: configured.hostAddress,
|
|
4059
|
+
meshHost,
|
|
4060
|
+
manualPairing: {
|
|
4061
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
4062
|
+
joinImplemented: true,
|
|
4063
|
+
protocol: 'standalone_command_direct_v1',
|
|
4064
|
+
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.',
|
|
4065
|
+
},
|
|
4066
|
+
};
|
|
4067
|
+
} catch (e: any) {
|
|
4068
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
case 'create_mesh_host_pairing_token': {
|
|
4073
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4074
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4075
|
+
try {
|
|
4076
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
4077
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
4078
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
4079
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
4080
|
+
});
|
|
4081
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
4082
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
4083
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4084
|
+
return {
|
|
4085
|
+
success: true,
|
|
4086
|
+
code: 'mesh_host_pairing_token_created',
|
|
4087
|
+
meshId,
|
|
4088
|
+
token: created.token,
|
|
4089
|
+
tokenId: created.tokenId,
|
|
4090
|
+
expiresAt: created.expiresAt,
|
|
4091
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
4092
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
4093
|
+
};
|
|
4094
|
+
} catch (e: any) {
|
|
4095
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
case 'apply_mesh_host_join': {
|
|
4100
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4101
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4102
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
4103
|
+
? args.memberNode
|
|
4104
|
+
: null;
|
|
4105
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4106
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
4107
|
+
try {
|
|
4108
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
4109
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
4110
|
+
token,
|
|
4111
|
+
memberNode: memberNode as any,
|
|
4112
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
4113
|
+
});
|
|
4114
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
4115
|
+
if (!applied.accepted) {
|
|
4116
|
+
return {
|
|
4117
|
+
success: false,
|
|
4118
|
+
code: 'mesh_host_join_rejected',
|
|
4119
|
+
meshId,
|
|
4120
|
+
tokenId: applied.tokenId,
|
|
4121
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
4122
|
+
error: applied.reason,
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
4126
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4127
|
+
try {
|
|
4128
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
4129
|
+
appendLedgerEntry(meshId, {
|
|
4130
|
+
kind: 'node_joined',
|
|
4131
|
+
nodeId: applied.node.id,
|
|
4132
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
4133
|
+
});
|
|
4134
|
+
} catch { /* ledger append is best-effort */ }
|
|
4135
|
+
return {
|
|
4136
|
+
success: true,
|
|
4137
|
+
code: 'mesh_host_join_accepted',
|
|
4138
|
+
meshId,
|
|
4139
|
+
node: applied.node,
|
|
4140
|
+
tokenId: applied.tokenId,
|
|
4141
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
4142
|
+
};
|
|
4143
|
+
} catch (e: any) {
|
|
4144
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
case 'join_mesh_host_pairing': {
|
|
4149
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4150
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4151
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4152
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
4153
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4154
|
+
const mesh = meshRecord?.mesh;
|
|
4155
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4156
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4157
|
+
if (meshHost.role !== 'member') {
|
|
4158
|
+
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.' };
|
|
4159
|
+
}
|
|
4160
|
+
try {
|
|
4161
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
4162
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
4163
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
4164
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
4165
|
+
}
|
|
4166
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
4167
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
4168
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
4169
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
4170
|
+
? args.hostDaemonId.trim()
|
|
4171
|
+
: meshHost.hostDaemonId;
|
|
4172
|
+
let hostResult: any;
|
|
4173
|
+
let transport: string;
|
|
4174
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
4175
|
+
transport = 'mesh_command_dispatch';
|
|
4176
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
4177
|
+
meshId: hostMeshId,
|
|
4178
|
+
token,
|
|
4179
|
+
memberMeshId: meshId,
|
|
4180
|
+
memberNode,
|
|
4181
|
+
});
|
|
4182
|
+
} else if (meshHost.hostAddress) {
|
|
4183
|
+
transport = 'standalone_http_command';
|
|
4184
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
4185
|
+
const response = await fetch(commandUrl, {
|
|
4186
|
+
method: 'POST',
|
|
4187
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4188
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
4189
|
+
});
|
|
4190
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
4191
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
4192
|
+
} else {
|
|
4193
|
+
return {
|
|
4194
|
+
success: false,
|
|
4195
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
4196
|
+
meshId,
|
|
4197
|
+
meshHost,
|
|
4198
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
4199
|
+
};
|
|
4200
|
+
}
|
|
4201
|
+
if (!hostResult?.success) {
|
|
4202
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
4203
|
+
}
|
|
4204
|
+
const joined = meshRecord.inline
|
|
4205
|
+
? null
|
|
4206
|
+
: markMeshHostPairingJoined(meshId, {
|
|
4207
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
4208
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
4209
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
4210
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
4211
|
+
});
|
|
4212
|
+
if (joined) {
|
|
4213
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
4214
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4215
|
+
}
|
|
4216
|
+
return {
|
|
4217
|
+
success: true,
|
|
4218
|
+
code: 'mesh_host_join_applied',
|
|
4219
|
+
meshId,
|
|
4220
|
+
hostMeshId,
|
|
4221
|
+
transport,
|
|
4222
|
+
node: hostResult.node,
|
|
4223
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
4224
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
4225
|
+
hostResult,
|
|
4226
|
+
manualPairing: {
|
|
4227
|
+
status: 'paired',
|
|
4228
|
+
joinImplemented: true,
|
|
4229
|
+
protocol: 'standalone_command_direct_v1',
|
|
4230
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
4231
|
+
},
|
|
4232
|
+
};
|
|
4233
|
+
} catch (e: any) {
|
|
4234
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
|
|
1847
4238
|
case 'delete_mesh': {
|
|
1848
4239
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1849
4240
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -1937,6 +4328,8 @@ export class DaemonCommandRouter {
|
|
|
1937
4328
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1938
4329
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
1939
4330
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4331
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
4332
|
+
if (ownerFailure) return ownerFailure;
|
|
1940
4333
|
try {
|
|
1941
4334
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
1942
4335
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -1952,6 +4345,8 @@ export class DaemonCommandRouter {
|
|
|
1952
4345
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1953
4346
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
1954
4347
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4348
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
4349
|
+
if (ownerFailure) return ownerFailure;
|
|
1955
4350
|
try {
|
|
1956
4351
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
1957
4352
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -1973,6 +4368,8 @@ export class DaemonCommandRouter {
|
|
|
1973
4368
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
1974
4369
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1975
4370
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
4371
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
4372
|
+
if (ownerFailure) return ownerFailure;
|
|
1976
4373
|
try {
|
|
1977
4374
|
const { addNode } = await import('../config/mesh-config.js');
|
|
1978
4375
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -1983,7 +4380,18 @@ export class DaemonCommandRouter {
|
|
|
1983
4380
|
...(readOnly ? { readOnly: true } : {}),
|
|
1984
4381
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
1985
4382
|
};
|
|
1986
|
-
const
|
|
4383
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
4384
|
+
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
4385
|
+
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
4386
|
+
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
4387
|
+
const node = addNode(meshId, {
|
|
4388
|
+
workspace,
|
|
4389
|
+
...(repoRoot ? { repoRoot } : {}),
|
|
4390
|
+
...(daemonId ? { daemonId } : {}),
|
|
4391
|
+
...(machineId ? { machineId } : {}),
|
|
4392
|
+
...(policy ? { policy } : {}),
|
|
4393
|
+
...(role ? { role } : {}),
|
|
4394
|
+
});
|
|
1987
4395
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
1988
4396
|
return { success: true, node };
|
|
1989
4397
|
} catch (e: any) {
|
|
@@ -1995,6 +4403,8 @@ export class DaemonCommandRouter {
|
|
|
1995
4403
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1996
4404
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1997
4405
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4406
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
4407
|
+
if (ownerFailure) return ownerFailure;
|
|
1998
4408
|
try {
|
|
1999
4409
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2000
4410
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2023,6 +4433,8 @@ export class DaemonCommandRouter {
|
|
|
2023
4433
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2024
4434
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2025
4435
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4436
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
4437
|
+
if (ownerFailure) return ownerFailure;
|
|
2026
4438
|
try {
|
|
2027
4439
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2028
4440
|
const mesh = meshRecord?.mesh;
|
|
@@ -2048,131 +4460,91 @@ export class DaemonCommandRouter {
|
|
|
2048
4460
|
}
|
|
2049
4461
|
}
|
|
2050
4462
|
|
|
2051
|
-
case '
|
|
4463
|
+
case 'get_mesh_refine_config_schema': {
|
|
4464
|
+
return {
|
|
4465
|
+
success: true,
|
|
4466
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
4467
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
4468
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
4469
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
case 'validate_mesh_refine_config': {
|
|
4474
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4475
|
+
const mesh = args?.inlineMesh || {};
|
|
4476
|
+
const loaded = args?.config !== undefined
|
|
4477
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
4478
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
4479
|
+
const validation = loaded.config
|
|
4480
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
4481
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
4482
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
case 'suggest_mesh_refine_config': {
|
|
4486
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4487
|
+
const mesh = args?.inlineMesh || {};
|
|
4488
|
+
return {
|
|
4489
|
+
success: true,
|
|
4490
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
4491
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
4492
|
+
};
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
case 'plan_mesh_refine_node': {
|
|
2052
4496
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2053
4497
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2054
4498
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2055
|
-
|
|
4499
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
4500
|
+
const mesh = meshRecord?.mesh;
|
|
4501
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
4502
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
4503
|
+
return {
|
|
4504
|
+
success: true,
|
|
4505
|
+
dryRun: true,
|
|
4506
|
+
nodeId,
|
|
4507
|
+
workspace: node.workspace,
|
|
4508
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
4509
|
+
mergeWillRun: false,
|
|
4510
|
+
cleanupWillRun: false,
|
|
4511
|
+
};
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
case 'fast_forward_mesh_node': {
|
|
4515
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4516
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4517
|
+
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
4518
|
+
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
4519
|
+
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
4520
|
+
: undefined;
|
|
4521
|
+
if (!workspace && meshId && nodeId) {
|
|
2056
4522
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2057
4523
|
const mesh = meshRecord?.mesh;
|
|
2058
4524
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
return { success: false, error: `Refinery requires a local worktree node` };
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
const sourceNode = node.clonedFromNodeId
|
|
2066
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2067
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2068
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2069
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2070
|
-
|
|
2071
|
-
const { execFile } = await import('node:child_process');
|
|
2072
|
-
const { promisify } = await import('node:util');
|
|
2073
|
-
const execFileAsync = promisify(execFile);
|
|
2074
|
-
|
|
2075
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2076
|
-
const branch = branchStdout.trim();
|
|
2077
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2078
|
-
|
|
2079
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2080
|
-
const baseBranch = baseBranchStdout.trim();
|
|
2081
|
-
|
|
2082
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2083
|
-
if (validationSummary.status === 'failed') {
|
|
2084
|
-
return {
|
|
2085
|
-
success: false,
|
|
2086
|
-
code: 'validation_failed',
|
|
2087
|
-
convergenceStatus: 'blocked_review',
|
|
2088
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2089
|
-
branch,
|
|
2090
|
-
into: baseBranch,
|
|
2091
|
-
validationSummary,
|
|
2092
|
-
finalBranchConvergenceState: {
|
|
2093
|
-
branch,
|
|
2094
|
-
baseBranch,
|
|
2095
|
-
merged: false,
|
|
2096
|
-
removed: false,
|
|
2097
|
-
validation: 'failed',
|
|
2098
|
-
status: 'blocked_review',
|
|
2099
|
-
},
|
|
2100
|
-
};
|
|
2101
|
-
}
|
|
2102
|
-
if (validationSummary.status === 'skipped') {
|
|
2103
|
-
return {
|
|
2104
|
-
success: false,
|
|
2105
|
-
code: 'validation_unavailable',
|
|
2106
|
-
convergenceStatus: 'blocked_review',
|
|
2107
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2108
|
-
branch,
|
|
2109
|
-
into: baseBranch,
|
|
2110
|
-
validationSummary,
|
|
2111
|
-
finalBranchConvergenceState: {
|
|
2112
|
-
branch,
|
|
2113
|
-
baseBranch,
|
|
2114
|
-
merged: false,
|
|
2115
|
-
removed: false,
|
|
2116
|
-
validation: 'unavailable',
|
|
2117
|
-
status: 'blocked_review',
|
|
2118
|
-
},
|
|
2119
|
-
};
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
try {
|
|
2123
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2124
|
-
} catch (e: any) {
|
|
2125
|
-
return {
|
|
2126
|
-
success: false,
|
|
2127
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2128
|
-
validationSummary,
|
|
2129
|
-
finalBranchConvergenceState: {
|
|
2130
|
-
branch,
|
|
2131
|
-
baseBranch,
|
|
2132
|
-
merged: false,
|
|
2133
|
-
removed: false,
|
|
2134
|
-
validation: 'passed',
|
|
2135
|
-
status: 'not_mergeable',
|
|
2136
|
-
},
|
|
2137
|
-
};
|
|
4525
|
+
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
4526
|
+
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
4527
|
+
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
2138
4528
|
}
|
|
2139
|
-
|
|
2140
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
2141
|
-
meshId,
|
|
2142
|
-
nodeId,
|
|
2143
|
-
sessionCleanupMode: 'kill',
|
|
2144
|
-
inlineMesh: args?.inlineMesh,
|
|
2145
|
-
});
|
|
2146
|
-
|
|
2147
|
-
try {
|
|
2148
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2149
|
-
appendLedgerEntry(meshId, {
|
|
2150
|
-
kind: 'node_removed',
|
|
2151
|
-
nodeId,
|
|
2152
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
2153
|
-
});
|
|
2154
|
-
} catch {}
|
|
2155
|
-
|
|
2156
|
-
return {
|
|
2157
|
-
success: true,
|
|
2158
|
-
merged: true,
|
|
2159
|
-
branch,
|
|
2160
|
-
into: baseBranch,
|
|
2161
|
-
removeResult,
|
|
2162
|
-
validationSummary,
|
|
2163
|
-
finalBranchConvergenceState: {
|
|
2164
|
-
branch: baseBranch,
|
|
2165
|
-
mergedBranch: branch,
|
|
2166
|
-
baseBranch,
|
|
2167
|
-
merged: true,
|
|
2168
|
-
removed: removeResult?.success !== false,
|
|
2169
|
-
validation: 'passed',
|
|
2170
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2171
|
-
},
|
|
2172
|
-
};
|
|
2173
|
-
} catch (e: any) {
|
|
2174
|
-
return { success: false, error: e.message };
|
|
2175
4529
|
}
|
|
4530
|
+
const result = await (fastForwardMeshNode({
|
|
4531
|
+
meshId: meshId || undefined,
|
|
4532
|
+
nodeId: nodeId || undefined,
|
|
4533
|
+
workspace,
|
|
4534
|
+
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
4535
|
+
execute: args?.execute === true,
|
|
4536
|
+
dryRun: args?.dryRun === true,
|
|
4537
|
+
updateSubmodules: args?.updateSubmodules === true,
|
|
4538
|
+
submoduleIgnorePaths,
|
|
4539
|
+
}) as Promise<unknown>);
|
|
4540
|
+
return result as CommandRouterResult;
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
case 'refine_mesh_node': {
|
|
4544
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4545
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4546
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4547
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
2176
4548
|
}
|
|
2177
4549
|
|
|
2178
4550
|
case 'remove_mesh_node': {
|
|
@@ -2216,6 +4588,7 @@ export class DaemonCommandRouter {
|
|
|
2216
4588
|
} else {
|
|
2217
4589
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2218
4590
|
removed = removeNode(meshId, nodeId);
|
|
4591
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2219
4592
|
}
|
|
2220
4593
|
|
|
2221
4594
|
// Record in task ledger
|
|
@@ -2253,6 +4626,8 @@ export class DaemonCommandRouter {
|
|
|
2253
4626
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2254
4627
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2255
4628
|
if (!branch) return { success: false, error: 'branch required' };
|
|
4629
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
4630
|
+
if (ownerFailure) return ownerFailure;
|
|
2256
4631
|
|
|
2257
4632
|
try {
|
|
2258
4633
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2301,6 +4676,7 @@ export class DaemonCommandRouter {
|
|
|
2301
4676
|
policy: { ...(sourceNode.policy || {}) },
|
|
2302
4677
|
});
|
|
2303
4678
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
4679
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2304
4680
|
}
|
|
2305
4681
|
|
|
2306
4682
|
// Initialize submodules if policy allows (default: true)
|
|
@@ -2342,6 +4718,8 @@ export class DaemonCommandRouter {
|
|
|
2342
4718
|
case 'trigger_mesh_queue': {
|
|
2343
4719
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2344
4720
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4721
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
4722
|
+
if (ownerFailure) return ownerFailure;
|
|
2345
4723
|
try {
|
|
2346
4724
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2347
4725
|
if (meshId) {
|
|
@@ -2373,6 +4751,15 @@ export class DaemonCommandRouter {
|
|
|
2373
4751
|
mesh = getMesh(meshId);
|
|
2374
4752
|
}
|
|
2375
4753
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4754
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4755
|
+
if (!meshHost.canOwnCoordinator) {
|
|
4756
|
+
return {
|
|
4757
|
+
success: false,
|
|
4758
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
4759
|
+
meshId,
|
|
4760
|
+
cliType,
|
|
4761
|
+
};
|
|
4762
|
+
}
|
|
2376
4763
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2377
4764
|
|
|
2378
4765
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2392,7 +4779,16 @@ export class DaemonCommandRouter {
|
|
|
2392
4779
|
cliType,
|
|
2393
4780
|
};
|
|
2394
4781
|
}
|
|
2395
|
-
const
|
|
4782
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
4783
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
4784
|
+
: [];
|
|
4785
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
4786
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
4787
|
+
meshId,
|
|
4788
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
4789
|
+
liveSessionRecords: liveMeshSessions,
|
|
4790
|
+
allowCoordinatorSession: true,
|
|
4791
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2396
4792
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2397
4793
|
if (!cliType) {
|
|
2398
4794
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -2734,9 +5130,33 @@ export class DaemonCommandRouter {
|
|
|
2734
5130
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2735
5131
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2736
5132
|
try {
|
|
2737
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
5133
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
2738
5134
|
const mesh = meshRecord?.mesh;
|
|
2739
5135
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
5136
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
5137
|
+
|
|
5138
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
5139
|
+
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId).length;
|
|
5140
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
5141
|
+
if (!refreshRequested && pendingCoordinatorEventCount === 0) {
|
|
5142
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
5143
|
+
if (cachedStatus) {
|
|
5144
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
5145
|
+
meshId,
|
|
5146
|
+
command: 'mesh_status',
|
|
5147
|
+
refreshRequested,
|
|
5148
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
5149
|
+
});
|
|
5150
|
+
return cachedStatus;
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
const refreshReason = refreshRequested
|
|
5154
|
+
? 'explicit_refresh'
|
|
5155
|
+
: pendingCoordinatorEventCount > 0
|
|
5156
|
+
? 'pending_coordinator_events'
|
|
5157
|
+
: hadAggregateCache
|
|
5158
|
+
? 'stale_pending_cache_refresh'
|
|
5159
|
+
: 'cold_cache_miss';
|
|
2740
5160
|
|
|
2741
5161
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
2742
5162
|
const queue = getQueue(meshId);
|
|
@@ -2745,103 +5165,372 @@ export class DaemonCommandRouter {
|
|
|
2745
5165
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2746
5166
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2747
5167
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
2748
|
-
|
|
5168
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
5169
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
5170
|
+
: [];
|
|
5171
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
5172
|
+
|
|
5173
|
+
const localMachineId = loadConfig().machineId || '';
|
|
5174
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
5175
|
+
const directTruth = requireDirectPeerTruth
|
|
5176
|
+
? await hydrateInlineMeshDirectTruth({
|
|
5177
|
+
mesh,
|
|
5178
|
+
meshSource: meshRecord.source,
|
|
5179
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5180
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
5181
|
+
localMachineId,
|
|
5182
|
+
})
|
|
5183
|
+
: {
|
|
5184
|
+
directEvidenceCount: 0,
|
|
5185
|
+
localConfirmedCount: 0,
|
|
5186
|
+
peerAttemptedCount: 0,
|
|
5187
|
+
peerConfirmedCount: 0,
|
|
5188
|
+
unavailableNodeIds: [] as string[],
|
|
5189
|
+
};
|
|
5190
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
5191
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
5192
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
5193
|
+
&& !refreshRequested
|
|
5194
|
+
&& directTruth.directEvidenceCount > 0
|
|
5195
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
5196
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
5197
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
5198
|
+
: directTruth;
|
|
5199
|
+
const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
5200
|
+
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
5201
|
+
&& Array.isArray(mesh.nodes)
|
|
5202
|
+
&& mesh.nodes
|
|
5203
|
+
.filter((node: any) => unavailableDirectTruthNodeIds.has(String(node.id || node.nodeId || '')))
|
|
5204
|
+
.every((node: any) => node?.isLocalWorktree === true);
|
|
5205
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
5206
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
5207
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
5208
|
+
const failureResult = {
|
|
5209
|
+
success: false,
|
|
5210
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
5211
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
5212
|
+
sourceOfTruth: {
|
|
5213
|
+
membership: meshRecord.source === 'inline_cache'
|
|
5214
|
+
? 'coordinator_inline_mesh_cache'
|
|
5215
|
+
: meshRecord.source === 'local_config'
|
|
5216
|
+
? 'local_mesh_config'
|
|
5217
|
+
: 'inline_bootstrap_snapshot',
|
|
5218
|
+
coordinatorOwnsLiveTruth: false,
|
|
5219
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
5220
|
+
directPeerTruth: {
|
|
5221
|
+
required: true,
|
|
5222
|
+
satisfied: false,
|
|
5223
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
5224
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
5225
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
5226
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
5227
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
5228
|
+
},
|
|
5229
|
+
},
|
|
5230
|
+
};
|
|
5231
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
5232
|
+
meshId,
|
|
5233
|
+
command: 'mesh_status',
|
|
5234
|
+
refreshRequested,
|
|
5235
|
+
meshSource: meshRecord.source,
|
|
5236
|
+
directTruth,
|
|
5237
|
+
});
|
|
5238
|
+
return failureResult;
|
|
5239
|
+
}
|
|
5240
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
5241
|
+
const coordinatorHostname = osHostname();
|
|
5242
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
5243
|
+
mesh.coordinator?.preferredNodeId,
|
|
5244
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
5245
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
5246
|
+
);
|
|
5247
|
+
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
5248
|
+
? selectedCoordinatorNodeId
|
|
5249
|
+
: undefined;
|
|
5250
|
+
const refreshedAt = new Date().toISOString();
|
|
2749
5251
|
const nodeStatuses = [];
|
|
2750
|
-
for (const node of mesh.nodes || []) {
|
|
5252
|
+
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
5253
|
+
const nodeId = String(node.id || node.nodeId || '');
|
|
5254
|
+
const daemonId = readStringValue(node.daemonId);
|
|
5255
|
+
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
5256
|
+
const isSelfNode = Boolean(
|
|
5257
|
+
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
5258
|
+
) || Boolean(
|
|
5259
|
+
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
5260
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0);
|
|
5261
|
+
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
5262
|
+
localMachineId,
|
|
5263
|
+
localDaemonId: this.deps.statusInstanceId,
|
|
5264
|
+
coordinatorHostname,
|
|
5265
|
+
isSelfNode,
|
|
5266
|
+
});
|
|
2751
5267
|
const status: Record<string, unknown> = {
|
|
2752
|
-
nodeId
|
|
2753
|
-
machineLabel: node
|
|
5268
|
+
nodeId,
|
|
5269
|
+
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
5270
|
+
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
5271
|
+
? 'explicit_metadata'
|
|
5272
|
+
: 'workspace_host_provider_context',
|
|
2754
5273
|
workspace: node.workspace,
|
|
2755
5274
|
repoRoot: node.repoRoot,
|
|
2756
5275
|
isLocalWorktree: node.isLocalWorktree,
|
|
2757
5276
|
worktreeBranch: node.worktreeBranch,
|
|
2758
|
-
|
|
2759
|
-
|
|
5277
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
5278
|
+
daemonId,
|
|
5279
|
+
machineId: readMeshNodeMachineId(node as Record<string, unknown>) || node.machineId,
|
|
5280
|
+
machine: machineIdentity,
|
|
5281
|
+
machineStatus: node.machineStatus,
|
|
2760
5282
|
health: 'unknown',
|
|
2761
5283
|
providers: node.providers || [],
|
|
5284
|
+
providerPriority,
|
|
2762
5285
|
activeSessions: [],
|
|
5286
|
+
activeSessionDetails: [],
|
|
5287
|
+
launchReady: false,
|
|
2763
5288
|
};
|
|
2764
|
-
if (
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
5289
|
+
if (isSelfNode) {
|
|
5290
|
+
status.connection = {
|
|
5291
|
+
perspective: 'selected_coordinator',
|
|
5292
|
+
source: 'mesh_peer_status',
|
|
5293
|
+
state: 'self',
|
|
5294
|
+
transport: 'local',
|
|
5295
|
+
reported: true,
|
|
5296
|
+
reason: 'Selected coordinator daemon',
|
|
5297
|
+
lastStateChangeAt: refreshedAt,
|
|
5298
|
+
};
|
|
5299
|
+
} else if (daemonId) {
|
|
5300
|
+
const connection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
5301
|
+
status.connection = connection ?? {
|
|
5302
|
+
perspective: 'selected_coordinator',
|
|
5303
|
+
source: 'not_reported',
|
|
5304
|
+
state: 'unknown',
|
|
5305
|
+
transport: 'unknown',
|
|
5306
|
+
reported: false,
|
|
5307
|
+
reason: 'No live mesh peer telemetry reported by the selected coordinator yet.',
|
|
5308
|
+
};
|
|
5309
|
+
} else {
|
|
5310
|
+
status.connection = {
|
|
5311
|
+
perspective: 'selected_coordinator',
|
|
5312
|
+
source: 'not_reported',
|
|
5313
|
+
state: 'unknown',
|
|
5314
|
+
transport: 'unknown',
|
|
5315
|
+
reported: false,
|
|
5316
|
+
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
5317
|
+
};
|
|
5318
|
+
}
|
|
5319
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
5320
|
+
meshId,
|
|
5321
|
+
node,
|
|
5322
|
+
nodeId,
|
|
5323
|
+
liveSessionRecords: liveMeshSessions,
|
|
5324
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
5325
|
+
});
|
|
5326
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
5327
|
+
meshId,
|
|
5328
|
+
nodeId,
|
|
5329
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
5330
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
5331
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
5332
|
+
status.workspace = workspace || node.workspace;
|
|
5333
|
+
if (matchedLiveSessionRecords.length > 0) {
|
|
5334
|
+
const sessionIds = matchedLiveSessionRecords
|
|
5335
|
+
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
5336
|
+
.filter(Boolean);
|
|
5337
|
+
const providerTypes = matchedLiveSessionRecords
|
|
5338
|
+
.map((record: any) => readStringValue(record?.providerType))
|
|
5339
|
+
.filter(Boolean) as string[];
|
|
5340
|
+
status.activeSessions = sessionIds;
|
|
5341
|
+
status.activeSessionDetails = matchedLiveSessionRecords.map(summarizeMeshSessionRecord);
|
|
5342
|
+
if (providerTypes.length > 0) {
|
|
5343
|
+
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
if (workspace) {
|
|
5347
|
+
if (!fs.existsSync(workspace)) {
|
|
5348
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
5349
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
5350
|
+
let remoteProbeApplied = false;
|
|
5351
|
+
if (inlineTransitGit) {
|
|
5352
|
+
status.git = inlineTransitGit;
|
|
5353
|
+
status.health = inlineTransitGit.isGitRepo
|
|
5354
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
5355
|
+
: 'degraded';
|
|
5356
|
+
const connection = readObjectRecord(status.connection);
|
|
5357
|
+
const connectionState = readStringValue(connection.state);
|
|
5358
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5359
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5360
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5361
|
+
}
|
|
5362
|
+
remoteProbeApplied = true;
|
|
5363
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
5364
|
+
try {
|
|
5365
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
5366
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5367
|
+
daemonId,
|
|
5368
|
+
workspace,
|
|
5369
|
+
timeoutMs: 8000,
|
|
5370
|
+
});
|
|
5371
|
+
if (remoteGit) {
|
|
5372
|
+
status.git = remoteGit;
|
|
5373
|
+
status.health = remoteGit.isGitRepo
|
|
5374
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
5375
|
+
: 'degraded';
|
|
5376
|
+
const connection = readObjectRecord(status.connection);
|
|
5377
|
+
const connectionState = readStringValue(connection.state);
|
|
5378
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5379
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5380
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5381
|
+
}
|
|
5382
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
5383
|
+
remoteProbeApplied = true;
|
|
5384
|
+
}
|
|
5385
|
+
} catch {
|
|
5386
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
5387
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
5388
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
5389
|
+
status.connection = refreshedConnection;
|
|
5390
|
+
try {
|
|
5391
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
5392
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5393
|
+
daemonId,
|
|
5394
|
+
workspace,
|
|
5395
|
+
timeoutMs: 12000,
|
|
5396
|
+
});
|
|
5397
|
+
if (remoteGit) {
|
|
5398
|
+
status.git = remoteGit;
|
|
5399
|
+
status.health = remoteGit.isGitRepo
|
|
5400
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
5401
|
+
: 'degraded';
|
|
5402
|
+
const connection = readObjectRecord(status.connection);
|
|
5403
|
+
const connectionState = readStringValue(connection.state);
|
|
5404
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
5405
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
5406
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
5407
|
+
}
|
|
5408
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
5409
|
+
remoteProbeApplied = true;
|
|
5410
|
+
}
|
|
5411
|
+
} catch {
|
|
5412
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
2792
5415
|
}
|
|
2793
5416
|
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
5417
|
+
if (!remoteProbeApplied) {
|
|
5418
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
5419
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
5420
|
+
&& !isSelfNode
|
|
5421
|
+
&& !!daemonId
|
|
5422
|
+
&& (
|
|
5423
|
+
readStringValue(status.machineStatus) === 'online'
|
|
5424
|
+
|| readStringValue(status.health) === 'online'
|
|
5425
|
+
|| connectionState === 'connecting'
|
|
5426
|
+
|| connectionState === 'connected'
|
|
5427
|
+
|| connectionState === 'unknown'
|
|
5428
|
+
);
|
|
5429
|
+
if (pendingPeerGitProbe) {
|
|
5430
|
+
status.gitProbePending = true;
|
|
5431
|
+
status.health = 'unknown';
|
|
5432
|
+
}
|
|
5433
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
5434
|
+
status,
|
|
5435
|
+
node,
|
|
5436
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
5437
|
+
)) {
|
|
5438
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5439
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
5440
|
+
nodeStatuses.push(status);
|
|
5441
|
+
continue;
|
|
5442
|
+
}
|
|
5443
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
5444
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5445
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
5446
|
+
nodeStatuses.push(status);
|
|
5447
|
+
continue;
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
} else {
|
|
5451
|
+
try {
|
|
5452
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
5453
|
+
status.git = gitStatus;
|
|
5454
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
5455
|
+
if (gitStatus.isGitRepo) {
|
|
5456
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
5457
|
+
} else {
|
|
5458
|
+
status.health = 'degraded';
|
|
5459
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
5460
|
+
}
|
|
5461
|
+
} catch {
|
|
5462
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
5463
|
+
status.health = 'degraded';
|
|
5464
|
+
}
|
|
2805
5465
|
}
|
|
2806
|
-
|
|
2807
|
-
status.git = {
|
|
2808
|
-
workspace: node.workspace,
|
|
2809
|
-
repoRoot: node.workspace,
|
|
2810
|
-
isGitRepo: true,
|
|
2811
|
-
branch: branch || null,
|
|
2812
|
-
headCommit,
|
|
2813
|
-
headMessage,
|
|
2814
|
-
upstream,
|
|
2815
|
-
ahead,
|
|
2816
|
-
behind,
|
|
2817
|
-
staged,
|
|
2818
|
-
modified,
|
|
2819
|
-
untracked,
|
|
2820
|
-
deleted,
|
|
2821
|
-
renamed,
|
|
2822
|
-
hasConflicts: false,
|
|
2823
|
-
conflictFiles: [],
|
|
2824
|
-
stashCount: stashCount ? stashCount.split('\n').filter(Boolean).length : 0,
|
|
2825
|
-
lastCheckedAt: Date.now(),
|
|
2826
|
-
};
|
|
2827
|
-
status.health = branch ? (dirty ? 'dirty' : 'online') : 'degraded';
|
|
2828
|
-
} catch {
|
|
2829
|
-
status.health = 'degraded';
|
|
2830
5466
|
}
|
|
5467
|
+
} else {
|
|
5468
|
+
applyCachedInlineMeshNodeStatus(status, node);
|
|
2831
5469
|
}
|
|
5470
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
5471
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
2832
5472
|
nodeStatuses.push(status);
|
|
2833
5473
|
}
|
|
2834
5474
|
|
|
2835
|
-
|
|
5475
|
+
const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId);
|
|
5476
|
+
const statusResult = {
|
|
2836
5477
|
success: true,
|
|
2837
5478
|
meshId: mesh.id,
|
|
2838
5479
|
meshName: mesh.name,
|
|
2839
5480
|
repoIdentity: mesh.repoIdentity,
|
|
2840
5481
|
defaultBranch: mesh.defaultBranch,
|
|
5482
|
+
refreshedAt,
|
|
5483
|
+
meshHost,
|
|
5484
|
+
sourceOfTruth: {
|
|
5485
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
5486
|
+
? 'coordinator_inline_mesh_cache'
|
|
5487
|
+
: meshRecord?.source === 'local_config'
|
|
5488
|
+
? 'local_mesh_config'
|
|
5489
|
+
: 'inline_bootstrap_snapshot',
|
|
5490
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
5491
|
+
meshHost: {
|
|
5492
|
+
owner: 'mesh_host_daemon',
|
|
5493
|
+
localRole: meshHost.role,
|
|
5494
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
5495
|
+
hostNodeId: meshHost.hostNodeId,
|
|
5496
|
+
hostAddress: meshHost.hostAddress,
|
|
5497
|
+
},
|
|
5498
|
+
...(requireDirectPeerTruth ? {
|
|
5499
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
5500
|
+
directPeerTruth: {
|
|
5501
|
+
required: true,
|
|
5502
|
+
satisfied: directTruthSatisfied,
|
|
5503
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
5504
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
5505
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
5506
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
5507
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
5508
|
+
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
5509
|
+
},
|
|
5510
|
+
} : {}),
|
|
5511
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary'],
|
|
5512
|
+
},
|
|
5513
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
2841
5514
|
nodes: nodeStatuses,
|
|
2842
5515
|
queue: { tasks: queue, summary: queueSummary },
|
|
2843
5516
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
5517
|
+
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
2844
5518
|
};
|
|
5519
|
+
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
|
|
5520
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
5521
|
+
const returnedStatus = pendingCoordinatorEvents.length > 0
|
|
5522
|
+
? { ...rememberedStatus, pendingCoordinatorEvents }
|
|
5523
|
+
: rememberedStatus;
|
|
5524
|
+
logRepoMeshStatusDebug('return_live', {
|
|
5525
|
+
meshId,
|
|
5526
|
+
command: 'mesh_status',
|
|
5527
|
+
refreshRequested,
|
|
5528
|
+
refreshReason,
|
|
5529
|
+
meshSource: meshRecord.source,
|
|
5530
|
+
directTruth,
|
|
5531
|
+
summary: summarizeRepoMeshStatusDebug(returnedStatus),
|
|
5532
|
+
});
|
|
5533
|
+
return returnedStatus;
|
|
2845
5534
|
} catch (e: any) {
|
|
2846
5535
|
return { success: false, error: e.message };
|
|
2847
5536
|
}
|
|
@@ -2899,7 +5588,7 @@ export class DaemonCommandRouter {
|
|
|
2899
5588
|
|
|
2900
5589
|
// 3. Kill OS process if requested
|
|
2901
5590
|
if (killProcess) {
|
|
2902
|
-
const running = isIdeRunning(ideType);
|
|
5591
|
+
const running = await isIdeRunning(ideType);
|
|
2903
5592
|
if (running) {
|
|
2904
5593
|
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
|
|
2905
5594
|
const killed = await killIdeProcess(ideType);
|