@adhdev/daemon-core 0.9.82-rc.363 → 0.9.82-rc.365
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +2 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +116 -0
- package/dist/commands/router.d.ts +83 -0
- package/dist/index.js +1652 -1515
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1655 -1519
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/fsm-driver.d.ts +27 -0
- package/dist/system/hash.d.ts +8 -0
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +2 -1
- package/src/commands/med-family/cli-agent.ts +218 -0
- package/src/commands/med-family/fast-forward.ts +198 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +35 -0
- package/src/commands/med-family/mesh-crud.ts +788 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +131 -0
- package/src/commands/med-family/types.ts +120 -0
- package/src/commands/mesh-coordinator.ts +2 -2
- package/src/commands/router.ts +57 -1602
- package/src/config/mesh-config.ts +3 -2
- package/src/mesh/mesh-active-work.ts +59 -81
- package/src/providers/spec/fsm-driver.ts +56 -2
- package/src/system/hash.ts +23 -0
package/src/commands/router.ts
CHANGED
|
@@ -13,6 +13,9 @@ import { DaemonCdpManager } from '../cdp/manager.js';
|
|
|
13
13
|
import { registerExtensionProviders } from '../cdp/setup.js';
|
|
14
14
|
import { DaemonCommandHandler } from './handler.js';
|
|
15
15
|
import { lowFamilyRegistry } from './low-family/index.js';
|
|
16
|
+
import { medFamilyRegistry } from './med-family/index.js';
|
|
17
|
+
import { launchIde } from './med-family/ide.js';
|
|
18
|
+
import type { MedFamilyContext } from './med-family/index.js';
|
|
16
19
|
import { DaemonCliManager } from './cli-manager.js';
|
|
17
20
|
import { supportsExplicitSessionResume } from './cli-manager.js';
|
|
18
21
|
import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
|
|
@@ -114,7 +117,7 @@ function readProviderPriorityFromPolicy(policy: unknown): string[] {
|
|
|
114
117
|
* the field entirely (full backward compat). Routing is governed by required_tags;
|
|
115
118
|
* any legacy `role` field on the input is ignored.
|
|
116
119
|
*/
|
|
117
|
-
function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
|
|
120
|
+
export function normalizeProviderRoles(value: unknown): Array<{ providerType: string; maxParallel?: number }> {
|
|
118
121
|
if (!Array.isArray(value)) return [];
|
|
119
122
|
const byType = new Map<string, { providerType: string; maxParallel?: number }>();
|
|
120
123
|
for (const raw of value) {
|
|
@@ -130,13 +133,13 @@ function normalizeProviderRoles(value: unknown): Array<{ providerType: string; m
|
|
|
130
133
|
return [...byType.values()];
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
function readObjectRecord(value: unknown): Record<string, any> {
|
|
136
|
+
export function readObjectRecord(value: unknown): Record<string, any> {
|
|
134
137
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
135
138
|
? value as Record<string, any>
|
|
136
139
|
: {};
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
function readStringValue(...values: unknown[]): string | undefined {
|
|
142
|
+
export function readStringValue(...values: unknown[]): string | undefined {
|
|
140
143
|
for (const value of values) {
|
|
141
144
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
142
145
|
}
|
|
@@ -224,7 +227,7 @@ function normalizeMeshHostname(value: unknown): string | undefined {
|
|
|
224
227
|
return hostname.toLowerCase().replace(/\.$/, '');
|
|
225
228
|
}
|
|
226
229
|
|
|
227
|
-
function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
|
|
230
|
+
export function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
|
|
228
231
|
return readStringValue(
|
|
229
232
|
node.machineId,
|
|
230
233
|
node.machine_id,
|
|
@@ -1156,7 +1159,7 @@ const MESH_DIRECT_PROBE_REUSE_MS = readMeshTimeoutEnvMs('MESH_DIRECT_PROBE_REUSE
|
|
|
1156
1159
|
* Lives on the router instance so the gate spans separate mesh_status calls,
|
|
1157
1160
|
* which is exactly where the refresh storm happens.
|
|
1158
1161
|
*/
|
|
1159
|
-
class MeshGitProbeCache {
|
|
1162
|
+
export class MeshGitProbeCache {
|
|
1160
1163
|
private inflight = new Map<string, Promise<Record<string, unknown> | null>>();
|
|
1161
1164
|
private recent = new Map<string, { at: number; value: Record<string, unknown> }>();
|
|
1162
1165
|
|
|
@@ -1311,7 +1314,7 @@ async function probeRemoteMeshGitStatusWithRetry(args: {
|
|
|
1311
1314
|
return null;
|
|
1312
1315
|
}
|
|
1313
1316
|
|
|
1314
|
-
async function hydrateInlineMeshDirectTruth(args: {
|
|
1317
|
+
export async function hydrateInlineMeshDirectTruth(args: {
|
|
1315
1318
|
mesh: any;
|
|
1316
1319
|
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
1317
1320
|
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
@@ -3045,7 +3048,7 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
3045
3048
|
}
|
|
3046
3049
|
}
|
|
3047
3050
|
|
|
3048
|
-
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
3051
|
+
export function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
3049
3052
|
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
3050
3053
|
const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
|
|
3051
3054
|
displayCommand: command.displayCommand,
|
|
@@ -3470,7 +3473,7 @@ function normalizeCommandArgsWithInteractionId(args: any): Record<string, unknow
|
|
|
3470
3473
|
* basename to be a literal `*.json`, and re-joins under the verified parent so
|
|
3471
3474
|
* the returned path can't point outside the tree. Used by get/write_spec_source.
|
|
3472
3475
|
*/
|
|
3473
|
-
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
3476
|
+
export function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
3474
3477
|
const raw = hostAddress.trim();
|
|
3475
3478
|
if (!raw) throw new Error('hostAddress required');
|
|
3476
3479
|
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
@@ -3480,7 +3483,7 @@ function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
|
3480
3483
|
return url.toString();
|
|
3481
3484
|
}
|
|
3482
3485
|
|
|
3483
|
-
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
3486
|
+
export function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
3484
3487
|
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
3485
3488
|
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
3486
3489
|
? args.memberNode as Record<string, any>
|
|
@@ -3862,6 +3865,38 @@ export class DaemonCommandRouter {
|
|
|
3862
3865
|
this.deps.onMeshStateChange?.(meshId);
|
|
3863
3866
|
}
|
|
3864
3867
|
|
|
3868
|
+
/**
|
|
3869
|
+
* Build the MedFamilyContext handed to RF-ROUTER MED family handlers. Binds the
|
|
3870
|
+
* router-private collaborators those handlers need (mesh resolution, owner
|
|
3871
|
+
* gating, inline-cache mutation, worktree / session cleanup, refine job
|
|
3872
|
+
* starters, IDE stop/launch) plus the inline-mesh and git-probe caches. The
|
|
3873
|
+
* `launchIde` field closes over the freshly-built context so restart_session /
|
|
3874
|
+
* restart_ide invoke the IDE launch directly instead of recursing through
|
|
3875
|
+
* executeDaemonCommand('launch_ide').
|
|
3876
|
+
*/
|
|
3877
|
+
private buildMedFamilyContext(): MedFamilyContext {
|
|
3878
|
+
const ctx: MedFamilyContext = {
|
|
3879
|
+
deps: this.deps,
|
|
3880
|
+
getMeshForCommand: this.getMeshForCommand.bind(this),
|
|
3881
|
+
getCachedInlineMesh: this.getCachedInlineMesh.bind(this),
|
|
3882
|
+
requireMeshHostMutationOwner: this.requireMeshHostMutationOwner.bind(this),
|
|
3883
|
+
invalidateAggregateMeshStatus: this.invalidateAggregateMeshStatus.bind(this),
|
|
3884
|
+
updateInlineMeshNode: this.updateInlineMeshNode.bind(this),
|
|
3885
|
+
removeInlineMeshNode: this.removeInlineMeshNode.bind(this),
|
|
3886
|
+
normalizeMeshSessionCleanupMode: this.normalizeMeshSessionCleanupMode.bind(this),
|
|
3887
|
+
cleanupMeshSessions: this.cleanupMeshSessions.bind(this),
|
|
3888
|
+
cleanupLocalWorktreeNode: this.cleanupLocalWorktreeNode.bind(this),
|
|
3889
|
+
startMeshRefineJob: this.startMeshRefineJob.bind(this),
|
|
3890
|
+
batchRefineMeshNodes: this.batchRefineMeshNodes.bind(this),
|
|
3891
|
+
startMeshRefineBatchJob: this.startMeshRefineBatchJob.bind(this),
|
|
3892
|
+
stopIde: this.stopIde.bind(this),
|
|
3893
|
+
launchIde: (args: any) => launchIde(ctx, args),
|
|
3894
|
+
inlineMeshCache: this.inlineMeshCache,
|
|
3895
|
+
meshGitProbeCache: this.meshGitProbeCache,
|
|
3896
|
+
};
|
|
3897
|
+
return ctx;
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3865
3900
|
|
|
3866
3901
|
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
3867
3902
|
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
@@ -6183,6 +6218,19 @@ export class DaemonCommandRouter {
|
|
|
6183
6218
|
}, args);
|
|
6184
6219
|
}
|
|
6185
6220
|
|
|
6221
|
+
// RF-ROUTER MED family: medium-coupling commands (CLI/ACP agent, IDE
|
|
6222
|
+
// lifecycle, mesh CRUD, mesh queue, mesh host pairing, fast-forward /
|
|
6223
|
+
// refine convergence) are handled by the registry after the LOW family and
|
|
6224
|
+
// before the switch. Unlike LOW handlers, MED handlers need router-private
|
|
6225
|
+
// collaborators, so the context carries bound methods + the inline-mesh /
|
|
6226
|
+
// git-probe caches + the launchIde helper (which breaks the original
|
|
6227
|
+
// launch_ide ↔ restart_* self-recursion). A hit returns the same
|
|
6228
|
+
// CommandRouterResult the inlined case used to; a miss falls through.
|
|
6229
|
+
const medFamilyHandler = medFamilyRegistry.get(cmd);
|
|
6230
|
+
if (medFamilyHandler) {
|
|
6231
|
+
return await medFamilyHandler(this.buildMedFamilyContext(), args);
|
|
6232
|
+
}
|
|
6233
|
+
|
|
6186
6234
|
switch (cmd) {
|
|
6187
6235
|
// ─── CLI / ACP commands ───
|
|
6188
6236
|
case 'mesh_forward_event': {
|
|
@@ -6215,1599 +6263,6 @@ export class DaemonCommandRouter {
|
|
|
6215
6263
|
return { success: true };
|
|
6216
6264
|
}
|
|
6217
6265
|
|
|
6218
|
-
case 'launch_cli': {
|
|
6219
|
-
// The coordinator routing anchor (meshCoordinatorDaemonId) is stamped
|
|
6220
|
-
// upstream by mesh_launch_session, which resolves
|
|
6221
|
-
// coordinatorNode.daemonId || ctx.localDaemonId || ctx.localMachineId and
|
|
6222
|
-
// fail-closes for a remote node when none resolve. We deliberately do NOT
|
|
6223
|
-
// self-stamp this daemon's own id when the field is missing: for a
|
|
6224
|
-
// P2P-relayed remote worker launch, stamping the worker's own id would make
|
|
6225
|
-
// the self-forward gate (mesh-events-coordinator: daemonIdsEquivalent) treat the
|
|
6226
|
-
// worker as its own coordinator, suppressing the spontaneous completion-event
|
|
6227
|
-
// forward and leaving the event in the pending inbox until a read_chat
|
|
6228
|
-
// reconcile drains it. If the anchor is genuinely absent here, leave it
|
|
6229
|
-
// absent rather than poison the routing.
|
|
6230
|
-
const launchResult = await this.deps.cliManager.handleCliCommand(cmd, args);
|
|
6231
|
-
// Bug C fix (part 1): when launching a mesh node worker session, surface
|
|
6232
|
-
// bootstrapPending:true if the node's worktree bootstrap is still running.
|
|
6233
|
-
// This is informational — the launch is NOT blocked here (blocking is done
|
|
6234
|
-
// upstream by getWorktreeBootstrapLaunchBlock in the MCP layer).
|
|
6235
|
-
const meshNodeId = readStringValue((args?.settings as any)?.meshNodeId);
|
|
6236
|
-
const meshId = readStringValue((args?.settings as any)?.meshNodeFor);
|
|
6237
|
-
if (meshNodeId && meshId && launchResult?.success !== false) {
|
|
6238
|
-
try {
|
|
6239
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
6240
|
-
const meshObj = getMesh(meshId) ?? this.getCachedInlineMesh(meshId);
|
|
6241
|
-
const nodeObj = Array.isArray(meshObj?.nodes)
|
|
6242
|
-
? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, meshNodeId))
|
|
6243
|
-
: undefined;
|
|
6244
|
-
const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
|
|
6245
|
-
if (bootstrapStatus === 'running') {
|
|
6246
|
-
return { success: true, ...launchResult, bootstrapPending: true };
|
|
6247
|
-
}
|
|
6248
|
-
} catch { /* best-effort — do not fail launch for bootstrap probe errors */ }
|
|
6249
|
-
}
|
|
6250
|
-
return launchResult;
|
|
6251
|
-
}
|
|
6252
|
-
case 'stop_cli':
|
|
6253
|
-
case 'set_cli_view_mode':
|
|
6254
|
-
case 'record_provider_pty': {
|
|
6255
|
-
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
6256
|
-
}
|
|
6257
|
-
case 'agent_command': {
|
|
6258
|
-
// Relay-safety stamp: a dispatch carrying meshContext.coordinatorDaemonId
|
|
6259
|
-
// (mesh_send_task / queue assignment over P2P) is the worker daemon's chance
|
|
6260
|
-
// to persist the coordinator routing anchor onto the target session BEFORE the
|
|
6261
|
-
// turn runs. Without meshCoordinatorDaemonId on the session, the core forwarder
|
|
6262
|
-
// (injectMeshSystemMessage) cannot resolve a remote coordinator target, so the
|
|
6263
|
-
// completion event sits in the pending queue until a read_chat reconcile drains
|
|
6264
|
-
// it. Stamping here makes a reused/relaunched remote session relay-safe at
|
|
6265
|
-
// dispatch time even when it was not launched via mesh_launch_session.
|
|
6266
|
-
{
|
|
6267
|
-
const dispatchSessionId = readStringValue(args?.targetSessionId, (args as any)?.sessionId, (args as any)?.instanceId);
|
|
6268
|
-
const dispatchMeshContext = args?.meshContext as Record<string, unknown> | undefined;
|
|
6269
|
-
if (dispatchSessionId && dispatchMeshContext) {
|
|
6270
|
-
try {
|
|
6271
|
-
const inst = this.deps.instanceManager.getInstance(dispatchSessionId);
|
|
6272
|
-
if (inst && typeof inst.updateSettings === 'function') {
|
|
6273
|
-
const stamp = buildMeshWorkerRelayStamp(
|
|
6274
|
-
inst.getState?.()?.settings as Record<string, unknown> | undefined,
|
|
6275
|
-
{
|
|
6276
|
-
meshId: dispatchMeshContext.meshId,
|
|
6277
|
-
nodeId: dispatchMeshContext.nodeId,
|
|
6278
|
-
coordinatorDaemonId: dispatchMeshContext.coordinatorDaemonId,
|
|
6279
|
-
// Session-level anchor: preserved across the P2P dispatch to a
|
|
6280
|
-
// remote worker so its completion echoes back to the right session.
|
|
6281
|
-
coordinatorSessionId: dispatchMeshContext.coordinatorSessionId,
|
|
6282
|
-
},
|
|
6283
|
-
);
|
|
6284
|
-
if (stamp) inst.updateSettings(stamp);
|
|
6285
|
-
}
|
|
6286
|
-
} catch { /* best-effort — dispatch still proceeds without the stamp */ }
|
|
6287
|
-
}
|
|
6288
|
-
}
|
|
6289
|
-
const agentResult = await this.deps.cliManager.handleCliCommand(cmd, args);
|
|
6290
|
-
// Bug C fix (part 2): when dispatching a task to a mesh node session, override
|
|
6291
|
-
// the dispatch acknowledgement risk reason to 'bootstrap_still_running' when
|
|
6292
|
-
// the target node's worktree bootstrap is still running. Informational only —
|
|
6293
|
-
// dispatch is NOT blocked.
|
|
6294
|
-
const meshCtx = args?.meshContext as Record<string, unknown> | undefined;
|
|
6295
|
-
const dispatchNodeId = readStringValue(meshCtx?.nodeId);
|
|
6296
|
-
const dispatchMeshId = readStringValue(meshCtx?.meshId);
|
|
6297
|
-
if (dispatchNodeId && dispatchMeshId && agentResult?.success !== false) {
|
|
6298
|
-
try {
|
|
6299
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
6300
|
-
const meshObj = getMesh(dispatchMeshId) ?? this.getCachedInlineMesh(dispatchMeshId);
|
|
6301
|
-
const nodeObj = Array.isArray(meshObj?.nodes)
|
|
6302
|
-
? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, dispatchNodeId))
|
|
6303
|
-
: undefined;
|
|
6304
|
-
const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
|
|
6305
|
-
if (bootstrapStatus === 'running') {
|
|
6306
|
-
return {
|
|
6307
|
-
success: true,
|
|
6308
|
-
...agentResult,
|
|
6309
|
-
dispatchAcknowledgementRisk: true,
|
|
6310
|
-
dispatchAcknowledgementRiskReason: 'bootstrap_still_running',
|
|
6311
|
-
nextAction: 'Wait for worktree_bootstrap_complete event before dispatching work to this node.',
|
|
6312
|
-
};
|
|
6313
|
-
}
|
|
6314
|
-
} catch { /* best-effort */ }
|
|
6315
|
-
}
|
|
6316
|
-
return agentResult;
|
|
6317
|
-
}
|
|
6318
|
-
|
|
6319
|
-
// ─── Logs ───
|
|
6320
|
-
case 'list_saved_sessions': {
|
|
6321
|
-
const providerType = typeof args?.providerType === 'string'
|
|
6322
|
-
? args.providerType.trim()
|
|
6323
|
-
: typeof args?.agentType === 'string'
|
|
6324
|
-
? args.agentType.trim()
|
|
6325
|
-
: '';
|
|
6326
|
-
const kind = args?.kind === 'acp' ? 'acp' : 'cli';
|
|
6327
|
-
if (!providerType) {
|
|
6328
|
-
return { success: false, error: 'providerType required' };
|
|
6329
|
-
}
|
|
6330
|
-
|
|
6331
|
-
const wantsAll = args?.all === true;
|
|
6332
|
-
const offset = wantsAll ? 0 : Math.max(0, Number(args?.offset) || 0);
|
|
6333
|
-
const limit = wantsAll ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.min(100, Number(args?.limit) || 30));
|
|
6334
|
-
const requestedWorkspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
6335
|
-
const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
|
|
6336
|
-
? args.providerSessionId.trim()
|
|
6337
|
-
: typeof args?.activeProviderSessionId === 'string'
|
|
6338
|
-
? args.activeProviderSessionId.trim()
|
|
6339
|
-
: '';
|
|
6340
|
-
const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
|
|
6341
|
-
const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
|
|
6342
|
-
canonicalHistory: providerMeta?.nativeHistory,
|
|
6343
|
-
offset,
|
|
6344
|
-
limit,
|
|
6345
|
-
historyBehavior: providerMeta?.historyBehavior,
|
|
6346
|
-
scripts: providerMeta?.scripts as any,
|
|
6347
|
-
});
|
|
6348
|
-
const state = loadState();
|
|
6349
|
-
const savedSessions = getSavedProviderSessions(state, { providerType, kind });
|
|
6350
|
-
const recentSessions = getRecentActivity(state, 200)
|
|
6351
|
-
.filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
|
|
6352
|
-
const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
|
|
6353
|
-
const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
|
|
6354
|
-
const canResumeById = supportsExplicitSessionResume(providerMeta?.resume);
|
|
6355
|
-
|
|
6356
|
-
return {
|
|
6357
|
-
success: true,
|
|
6358
|
-
sessions: historySessions.map(session => {
|
|
6359
|
-
const saved = savedSessionById.get(session.historySessionId);
|
|
6360
|
-
const recent = recentSessionById.get(session.historySessionId);
|
|
6361
|
-
const workspace = saved?.workspace
|
|
6362
|
-
|| recent?.workspace
|
|
6363
|
-
|| session.workspace
|
|
6364
|
-
|| (requestedWorkspace && requestedProviderSessionId === session.historySessionId ? requestedWorkspace : undefined);
|
|
6365
|
-
return {
|
|
6366
|
-
id: session.historySessionId,
|
|
6367
|
-
providerSessionId: session.historySessionId,
|
|
6368
|
-
providerType,
|
|
6369
|
-
providerName: saved?.providerName || recent?.providerName || providerType,
|
|
6370
|
-
kind: saved?.kind || recent?.kind || kind,
|
|
6371
|
-
title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
|
|
6372
|
-
workspace,
|
|
6373
|
-
summaryMetadata: saved?.summaryMetadata || recent?.summaryMetadata,
|
|
6374
|
-
preview: session.preview,
|
|
6375
|
-
messageCount: session.messageCount,
|
|
6376
|
-
firstMessageAt: session.firstMessageAt,
|
|
6377
|
-
lastMessageAt: session.lastMessageAt,
|
|
6378
|
-
canResume: !!workspace && canResumeById,
|
|
6379
|
-
historySource: session.source,
|
|
6380
|
-
sourcePath: session.sourcePath,
|
|
6381
|
-
sourceMtimeMs: session.sourceMtimeMs,
|
|
6382
|
-
};
|
|
6383
|
-
}),
|
|
6384
|
-
hasMore,
|
|
6385
|
-
source,
|
|
6386
|
-
};
|
|
6387
|
-
}
|
|
6388
|
-
|
|
6389
|
-
// ─── restart_session: IDE / CLI / ACP unified ───
|
|
6390
|
-
case 'restart_session': {
|
|
6391
|
-
const targetType = args?.cliType || args?.agentType || args?.ideType;
|
|
6392
|
-
if (!targetType) throw new Error('cliType or ideType required');
|
|
6393
|
-
|
|
6394
|
-
// Check if IDE (in cdpManagers or provider category is ide)
|
|
6395
|
-
const isIde = this.deps.cdpManagers.has(targetType) ||
|
|
6396
|
-
this.deps.providerLoader.getMeta(targetType)?.category === 'ide';
|
|
6397
|
-
|
|
6398
|
-
if (isIde) {
|
|
6399
|
-
// IDE restart: stop (with process kill) → launch
|
|
6400
|
-
await this.stopIde(targetType, true);
|
|
6401
|
-
const launchResult = await this.executeDaemonCommand('launch_ide', { ideType: targetType, enableCdp: true, workspace: args?.workspace });
|
|
6402
|
-
return { success: true, restarted: true, ideType: targetType, launch: launchResult };
|
|
6403
|
-
}
|
|
6404
|
-
|
|
6405
|
-
// CLI/ACP restart: delegate to CliManager
|
|
6406
|
-
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
6407
|
-
}
|
|
6408
|
-
|
|
6409
|
-
// ─── IDE stop ───
|
|
6410
|
-
case 'stop_ide': {
|
|
6411
|
-
const ideType = args?.ideType;
|
|
6412
|
-
if (!ideType) throw new Error('ideType required');
|
|
6413
|
-
const killProcess = args?.killProcess !== false; // default true
|
|
6414
|
-
await this.stopIde(ideType, killProcess);
|
|
6415
|
-
try {
|
|
6416
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
6417
|
-
this.deps.detectedIdes.value = results;
|
|
6418
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
6419
|
-
} catch { /* ignore detection refresh errors */ }
|
|
6420
|
-
return { success: true, ideType, stopped: true, processKilled: killProcess };
|
|
6421
|
-
}
|
|
6422
|
-
|
|
6423
|
-
// ─── IDE restart ───
|
|
6424
|
-
case 'restart_ide': {
|
|
6425
|
-
const ideType = args?.ideType;
|
|
6426
|
-
if (!ideType) throw new Error('ideType required');
|
|
6427
|
-
await this.stopIde(ideType, true); // always kill process on restart
|
|
6428
|
-
const launchResult = await this.executeDaemonCommand('launch_ide', { ideType, enableCdp: true, workspace: args?.workspace });
|
|
6429
|
-
return { success: true, ideType, restarted: true, launch: launchResult };
|
|
6430
|
-
}
|
|
6431
|
-
|
|
6432
|
-
// ─── IDE launch + CDP connect ───
|
|
6433
|
-
case 'launch_ide': {
|
|
6434
|
-
const ideKey = args?.ideId || args?.ideType;
|
|
6435
|
-
const resolvedWorkspace = resolveIdeLaunchWorkspace(
|
|
6436
|
-
{
|
|
6437
|
-
workspace: args?.workspace,
|
|
6438
|
-
workspaceId: args?.workspaceId,
|
|
6439
|
-
useDefaultWorkspace: args?.useDefaultWorkspace,
|
|
6440
|
-
},
|
|
6441
|
-
loadConfig(),
|
|
6442
|
-
);
|
|
6443
|
-
const launchArgs = {
|
|
6444
|
-
ideId: ideKey,
|
|
6445
|
-
workspace: resolvedWorkspace,
|
|
6446
|
-
newWindow: args?.newWindow,
|
|
6447
|
-
};
|
|
6448
|
-
LOG.info('LaunchIDE', `target=${ideKey || 'auto'}`);
|
|
6449
|
-
const result = await launchWithCdp(launchArgs);
|
|
6450
|
-
|
|
6451
|
-
if (result.success && result.port && result.ideId && !this.deps.cdpManagers.has(result.ideId)) {
|
|
6452
|
-
const logFn = this.deps.getCdpLogFn
|
|
6453
|
-
? this.deps.getCdpLogFn(result.ideId)
|
|
6454
|
-
: LOG.forComponent(`CDP:${result.ideId}`).asLogFn();
|
|
6455
|
-
const provider = this.deps.providerLoader.getMeta(result.ideId);
|
|
6456
|
-
const manager = new DaemonCdpManager(result.port, logFn, undefined, provider?.targetFilter);
|
|
6457
|
-
const connected = await manager.connect();
|
|
6458
|
-
if (connected) {
|
|
6459
|
-
// Register active extension providers for this IDE in CDP manager
|
|
6460
|
-
registerExtensionProviders(this.deps.providerLoader, manager, result.ideId);
|
|
6461
|
-
this.deps.cdpManagers.set(result.ideId, manager);
|
|
6462
|
-
LOG.info('CDP', `Connected: ${result.ideId} (port ${result.port})`);
|
|
6463
|
-
LOG.info('CDP', `${this.deps.cdpManagers.size} IDE(s) connected`);
|
|
6464
|
-
|
|
6465
|
-
// Notify consumer (e.g. setupIdeInstance)
|
|
6466
|
-
this.deps.onCdpManagerCreated?.(result.ideId, manager);
|
|
6467
|
-
}
|
|
6468
|
-
}
|
|
6469
|
-
this.deps.onIdeConnected?.();
|
|
6470
|
-
try {
|
|
6471
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
6472
|
-
this.deps.detectedIdes.value = results;
|
|
6473
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
6474
|
-
} catch { /* ignore detection refresh errors */ }
|
|
6475
|
-
if (result.success && resolvedWorkspace) {
|
|
6476
|
-
try {
|
|
6477
|
-
const next = appendRecentActivity(loadState(), {
|
|
6478
|
-
kind: 'ide',
|
|
6479
|
-
providerType: result.ideId || ideKey,
|
|
6480
|
-
providerName: result.ideId || ideKey,
|
|
6481
|
-
workspace: resolvedWorkspace,
|
|
6482
|
-
title: result.ideId || ideKey,
|
|
6483
|
-
});
|
|
6484
|
-
saveState(next);
|
|
6485
|
-
} catch { /* ignore activity persist errors */ }
|
|
6486
|
-
} else if (result.success && (result.ideId || ideKey)) {
|
|
6487
|
-
try {
|
|
6488
|
-
saveState(appendRecentActivity(loadState(), {
|
|
6489
|
-
kind: 'ide',
|
|
6490
|
-
providerType: result.ideId || ideKey,
|
|
6491
|
-
providerName: result.ideId || ideKey,
|
|
6492
|
-
title: result.ideId || ideKey,
|
|
6493
|
-
}));
|
|
6494
|
-
} catch { /* ignore activity persist errors */ }
|
|
6495
|
-
}
|
|
6496
|
-
return { ...result };
|
|
6497
|
-
}
|
|
6498
|
-
|
|
6499
|
-
// ─── Detect providers ───
|
|
6500
|
-
case 'detect_provider': {
|
|
6501
|
-
const providerType = typeof args?.providerType === 'string' ? args.providerType.trim() : '';
|
|
6502
|
-
if (!providerType) return { success: false, error: 'providerType is required' };
|
|
6503
|
-
const normalizedType = this.deps.providerLoader.resolveAlias(providerType);
|
|
6504
|
-
const provider = this.deps.providerLoader.getByAlias(providerType);
|
|
6505
|
-
if (!provider) return { success: false, error: `Provider not found: ${providerType}` };
|
|
6506
|
-
if (provider.category !== 'cli' && provider.category !== 'acp') {
|
|
6507
|
-
return { success: false, error: `Provider detection is only supported for CLI/ACP providers: ${providerType}` };
|
|
6508
|
-
}
|
|
6509
|
-
if (!this.deps.providerLoader.isMachineProviderEnabled(normalizedType)) {
|
|
6510
|
-
return { success: false, error: `Provider is disabled on this machine: ${providerType}` };
|
|
6511
|
-
}
|
|
6512
|
-
const detected = await detectCLI(normalizedType, this.deps.providerLoader, { includeVersion: false });
|
|
6513
|
-
this.deps.providerLoader.setCliDetectionResults([{
|
|
6514
|
-
id: normalizedType,
|
|
6515
|
-
installed: !!detected,
|
|
6516
|
-
path: detected?.path,
|
|
6517
|
-
}], false);
|
|
6518
|
-
this.deps.onStatusChange?.();
|
|
6519
|
-
return {
|
|
6520
|
-
success: true,
|
|
6521
|
-
providerType: normalizedType,
|
|
6522
|
-
detected: !!detected,
|
|
6523
|
-
path: detected?.path || null,
|
|
6524
|
-
};
|
|
6525
|
-
}
|
|
6526
|
-
|
|
6527
|
-
// ─── Detect IDEs ───
|
|
6528
|
-
case 'detect_ides': {
|
|
6529
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
6530
|
-
this.deps.detectedIdes.value = results;
|
|
6531
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
6532
|
-
return { success: true, detectedInfo: results };
|
|
6533
|
-
}
|
|
6534
|
-
|
|
6535
|
-
// ─── Mesh CRUD (local meshes.json) ───
|
|
6536
|
-
case 'list_meshes': {
|
|
6537
|
-
try {
|
|
6538
|
-
const { listMeshes } = await import('../config/mesh-config.js');
|
|
6539
|
-
return { success: true, meshes: listMeshes() };
|
|
6540
|
-
} catch (e: any) {
|
|
6541
|
-
return { success: false, error: e.message };
|
|
6542
|
-
}
|
|
6543
|
-
}
|
|
6544
|
-
|
|
6545
|
-
case 'get_mesh': {
|
|
6546
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6547
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6548
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6549
|
-
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
6550
|
-
|
|
6551
|
-
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
6552
|
-
// Only an explicit refresh fans out a blocking peer probe.
|
|
6553
|
-
// Default loads are satisfied from held standing-state git truth.
|
|
6554
|
-
const probeRemotePeers = args?.refresh === true || args?.forceRefresh === true;
|
|
6555
|
-
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
6556
|
-
mesh: meshRecord.mesh,
|
|
6557
|
-
meshSource: meshRecord.source,
|
|
6558
|
-
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6559
|
-
getMeshPeerConnectionStatus: this.deps.getMeshPeerConnectionStatus,
|
|
6560
|
-
statusInstanceId: this.deps.statusInstanceId,
|
|
6561
|
-
localMachineId: loadConfig().machineId || '',
|
|
6562
|
-
probeRemotePeers,
|
|
6563
|
-
probeCache: this.meshGitProbeCache,
|
|
6564
|
-
});
|
|
6565
|
-
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
6566
|
-
const sourceOfTruth = {
|
|
6567
|
-
membership: meshRecord.source === 'inline_cache'
|
|
6568
|
-
? 'coordinator_inline_mesh_cache'
|
|
6569
|
-
: meshRecord.source === 'local_config'
|
|
6570
|
-
? 'local_mesh_config'
|
|
6571
|
-
: 'inline_bootstrap_snapshot',
|
|
6572
|
-
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
6573
|
-
directPeerTruth: {
|
|
6574
|
-
required: requireDirectPeerTruth,
|
|
6575
|
-
satisfied: directTruthSatisfied,
|
|
6576
|
-
directEvidenceCount: directTruth.directEvidenceCount,
|
|
6577
|
-
localConfirmedCount: directTruth.localConfirmedCount,
|
|
6578
|
-
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
6579
|
-
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
6580
|
-
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
6581
|
-
},
|
|
6582
|
-
};
|
|
6583
|
-
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
6584
|
-
return {
|
|
6585
|
-
success: false,
|
|
6586
|
-
code: 'mesh_direct_peer_truth_unavailable',
|
|
6587
|
-
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
6588
|
-
sourceOfTruth,
|
|
6589
|
-
};
|
|
6590
|
-
}
|
|
6591
|
-
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
6592
|
-
}
|
|
6593
|
-
|
|
6594
|
-
case 'create_mesh': {
|
|
6595
|
-
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
6596
|
-
const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
|
|
6597
|
-
const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
|
|
6598
|
-
const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
|
|
6599
|
-
if (!name) return { success: false, error: 'name required' };
|
|
6600
|
-
try {
|
|
6601
|
-
const { createMesh } = await import('../config/mesh-config.js');
|
|
6602
|
-
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
6603
|
-
? args.meshHost
|
|
6604
|
-
: undefined;
|
|
6605
|
-
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
6606
|
-
return { success: true, mesh };
|
|
6607
|
-
} catch (e: any) {
|
|
6608
|
-
return { success: false, error: e.message };
|
|
6609
|
-
}
|
|
6610
|
-
}
|
|
6611
|
-
|
|
6612
|
-
case 'update_mesh': {
|
|
6613
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6614
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6615
|
-
try {
|
|
6616
|
-
const { updateMesh } = await import('../config/mesh-config.js');
|
|
6617
|
-
const patch: Record<string, unknown> = {};
|
|
6618
|
-
if (typeof args?.name === 'string') patch.name = args.name;
|
|
6619
|
-
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
6620
|
-
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
6621
|
-
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
6622
|
-
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
6623
|
-
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
6624
|
-
const mesh = updateMesh(meshId, patch as any);
|
|
6625
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6626
|
-
this.inlineMeshCache.set(meshId, mesh);
|
|
6627
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6628
|
-
return { success: true, mesh };
|
|
6629
|
-
} catch (e: any) {
|
|
6630
|
-
return { success: false, error: e.message };
|
|
6631
|
-
}
|
|
6632
|
-
}
|
|
6633
|
-
|
|
6634
|
-
case 'get_mesh_host_pairing': {
|
|
6635
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6636
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6637
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6638
|
-
const mesh = meshRecord?.mesh;
|
|
6639
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6640
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
6641
|
-
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
6642
|
-
return {
|
|
6643
|
-
success: true,
|
|
6644
|
-
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
6645
|
-
meshId,
|
|
6646
|
-
hostAddress: meshHost.hostAddress,
|
|
6647
|
-
meshHost,
|
|
6648
|
-
manualPairing: {
|
|
6649
|
-
status: pairingStatus,
|
|
6650
|
-
joinImplemented: true,
|
|
6651
|
-
protocol: 'standalone_command_direct_v1',
|
|
6652
|
-
description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
|
|
6653
|
-
},
|
|
6654
|
-
};
|
|
6655
|
-
}
|
|
6656
|
-
|
|
6657
|
-
case 'configure_mesh_host_pairing': {
|
|
6658
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6659
|
-
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
6660
|
-
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
6661
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6662
|
-
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
6663
|
-
try {
|
|
6664
|
-
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
6665
|
-
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
6666
|
-
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
6667
|
-
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
6668
|
-
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
6669
|
-
return {
|
|
6670
|
-
success: true,
|
|
6671
|
-
code: 'mesh_host_pairing_pending',
|
|
6672
|
-
meshId,
|
|
6673
|
-
hostAddress: configured.hostAddress,
|
|
6674
|
-
meshHost,
|
|
6675
|
-
manualPairing: {
|
|
6676
|
-
status: meshHost.pairing?.status || 'pairing',
|
|
6677
|
-
joinImplemented: true,
|
|
6678
|
-
protocol: 'standalone_command_direct_v1',
|
|
6679
|
-
description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
|
|
6680
|
-
},
|
|
6681
|
-
};
|
|
6682
|
-
} catch (e: any) {
|
|
6683
|
-
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
6684
|
-
}
|
|
6685
|
-
}
|
|
6686
|
-
|
|
6687
|
-
case 'create_mesh_host_pairing_token': {
|
|
6688
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6689
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6690
|
-
try {
|
|
6691
|
-
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
6692
|
-
const created = createMeshHostPairingToken(meshId, {
|
|
6693
|
-
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
6694
|
-
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
6695
|
-
});
|
|
6696
|
-
if (!created) return { success: false, error: 'Mesh not found' };
|
|
6697
|
-
this.inlineMeshCache.set(meshId, created.mesh);
|
|
6698
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6699
|
-
return {
|
|
6700
|
-
success: true,
|
|
6701
|
-
code: 'mesh_host_pairing_token_created',
|
|
6702
|
-
meshId,
|
|
6703
|
-
token: created.token,
|
|
6704
|
-
tokenId: created.tokenId,
|
|
6705
|
-
expiresAt: created.expiresAt,
|
|
6706
|
-
meshHost: resolveMeshHostStatus(created.mesh),
|
|
6707
|
-
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
6708
|
-
};
|
|
6709
|
-
} catch (e: any) {
|
|
6710
|
-
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
6711
|
-
}
|
|
6712
|
-
}
|
|
6713
|
-
|
|
6714
|
-
case 'apply_mesh_host_join': {
|
|
6715
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6716
|
-
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
6717
|
-
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
6718
|
-
? args.memberNode
|
|
6719
|
-
: null;
|
|
6720
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6721
|
-
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
6722
|
-
try {
|
|
6723
|
-
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
6724
|
-
const applied = applyMeshHostJoinRequest(meshId, {
|
|
6725
|
-
token,
|
|
6726
|
-
memberNode: memberNode as any,
|
|
6727
|
-
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
6728
|
-
});
|
|
6729
|
-
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
6730
|
-
if (!applied.accepted) {
|
|
6731
|
-
return {
|
|
6732
|
-
success: false,
|
|
6733
|
-
code: 'mesh_host_join_rejected',
|
|
6734
|
-
meshId,
|
|
6735
|
-
tokenId: applied.tokenId,
|
|
6736
|
-
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
6737
|
-
error: applied.reason,
|
|
6738
|
-
};
|
|
6739
|
-
}
|
|
6740
|
-
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
6741
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6742
|
-
try {
|
|
6743
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
6744
|
-
appendLedgerEntry(meshId, {
|
|
6745
|
-
kind: 'node_joined',
|
|
6746
|
-
nodeId: applied.node.id,
|
|
6747
|
-
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
6748
|
-
});
|
|
6749
|
-
} catch { /* ledger append is best-effort */ }
|
|
6750
|
-
return {
|
|
6751
|
-
success: true,
|
|
6752
|
-
code: 'mesh_host_join_accepted',
|
|
6753
|
-
meshId,
|
|
6754
|
-
node: applied.node,
|
|
6755
|
-
tokenId: applied.tokenId,
|
|
6756
|
-
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
6757
|
-
};
|
|
6758
|
-
} catch (e: any) {
|
|
6759
|
-
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
6760
|
-
}
|
|
6761
|
-
}
|
|
6762
|
-
|
|
6763
|
-
case 'join_mesh_host_pairing': {
|
|
6764
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6765
|
-
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
6766
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6767
|
-
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
6768
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6769
|
-
const mesh = meshRecord?.mesh;
|
|
6770
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6771
|
-
const meshHost = resolveMeshHostStatus(mesh);
|
|
6772
|
-
if (meshHost.role !== 'member') {
|
|
6773
|
-
return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
|
|
6774
|
-
}
|
|
6775
|
-
try {
|
|
6776
|
-
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
6777
|
-
const tokenId = tokenIdForManualPairing(token);
|
|
6778
|
-
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
6779
|
-
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
6780
|
-
}
|
|
6781
|
-
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
6782
|
-
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
6783
|
-
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
6784
|
-
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
6785
|
-
? args.hostDaemonId.trim()
|
|
6786
|
-
: meshHost.hostDaemonId;
|
|
6787
|
-
let hostResult: any;
|
|
6788
|
-
let transport: string;
|
|
6789
|
-
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
6790
|
-
transport = 'mesh_command_dispatch';
|
|
6791
|
-
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
6792
|
-
meshId: hostMeshId,
|
|
6793
|
-
token,
|
|
6794
|
-
memberMeshId: meshId,
|
|
6795
|
-
memberNode,
|
|
6796
|
-
});
|
|
6797
|
-
} else if (meshHost.hostAddress) {
|
|
6798
|
-
transport = 'standalone_http_command';
|
|
6799
|
-
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
6800
|
-
const response = await fetch(commandUrl, {
|
|
6801
|
-
method: 'POST',
|
|
6802
|
-
headers: { 'Content-Type': 'application/json' },
|
|
6803
|
-
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
6804
|
-
});
|
|
6805
|
-
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
6806
|
-
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
6807
|
-
} else {
|
|
6808
|
-
return {
|
|
6809
|
-
success: false,
|
|
6810
|
-
code: 'mesh_host_join_transport_unavailable',
|
|
6811
|
-
meshId,
|
|
6812
|
-
meshHost,
|
|
6813
|
-
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
6814
|
-
};
|
|
6815
|
-
}
|
|
6816
|
-
if (!hostResult?.success) {
|
|
6817
|
-
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
6818
|
-
}
|
|
6819
|
-
const joined = meshRecord.inline
|
|
6820
|
-
? null
|
|
6821
|
-
: markMeshHostPairingJoined(meshId, {
|
|
6822
|
-
tokenId: hostResult.tokenId || tokenId,
|
|
6823
|
-
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
6824
|
-
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
6825
|
-
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
6826
|
-
});
|
|
6827
|
-
if (joined) {
|
|
6828
|
-
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
6829
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6830
|
-
}
|
|
6831
|
-
return {
|
|
6832
|
-
success: true,
|
|
6833
|
-
code: 'mesh_host_join_applied',
|
|
6834
|
-
meshId,
|
|
6835
|
-
hostMeshId,
|
|
6836
|
-
transport,
|
|
6837
|
-
node: hostResult.node,
|
|
6838
|
-
tokenId: hostResult.tokenId || tokenId,
|
|
6839
|
-
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
6840
|
-
hostResult,
|
|
6841
|
-
manualPairing: {
|
|
6842
|
-
status: 'paired',
|
|
6843
|
-
joinImplemented: true,
|
|
6844
|
-
protocol: 'standalone_command_direct_v1',
|
|
6845
|
-
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
6846
|
-
},
|
|
6847
|
-
};
|
|
6848
|
-
} catch (e: any) {
|
|
6849
|
-
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
6850
|
-
}
|
|
6851
|
-
}
|
|
6852
|
-
|
|
6853
|
-
case 'delete_mesh': {
|
|
6854
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6855
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6856
|
-
try {
|
|
6857
|
-
const { deleteMesh } = await import('../config/mesh-config.js');
|
|
6858
|
-
const deleted = deleteMesh(meshId);
|
|
6859
|
-
return { success: true, deleted };
|
|
6860
|
-
} catch (e: any) {
|
|
6861
|
-
return { success: false, error: e.message };
|
|
6862
|
-
}
|
|
6863
|
-
}
|
|
6864
|
-
|
|
6865
|
-
case 'get_mesh_queue': {
|
|
6866
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6867
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6868
|
-
try {
|
|
6869
|
-
const { getMeshQueueStats, getQueue, describeTaskDependencyState } = await import('../mesh/mesh-work-queue.js');
|
|
6870
|
-
const status = Array.isArray(args?.status)
|
|
6871
|
-
? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
|
|
6872
|
-
: undefined;
|
|
6873
|
-
const rawQueue = getQueue(meshId, { status: status as any });
|
|
6874
|
-
// M1: annotate dependency state at view time (waitingOn / dependenciesSatisfied).
|
|
6875
|
-
const statusById = new Map(getQueue(meshId).map(task => [task.id, task.status]));
|
|
6876
|
-
const queue = rawQueue.map(task =>
|
|
6877
|
-
Array.isArray(task.dependsOn) && task.dependsOn.length > 0
|
|
6878
|
-
? { ...task, ...describeTaskDependencyState(task, statusById) }
|
|
6879
|
-
: task);
|
|
6880
|
-
const summary = getMeshQueueStats(meshId);
|
|
6881
|
-
return {
|
|
6882
|
-
success: true,
|
|
6883
|
-
queue,
|
|
6884
|
-
summary,
|
|
6885
|
-
sourceOfTruth: {
|
|
6886
|
-
kind: 'mesh_work_queue_file',
|
|
6887
|
-
activeStatuses: ['pending', 'assigned'],
|
|
6888
|
-
historicalStatuses: ['completed', 'failed', 'cancelled'],
|
|
6889
|
-
notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
|
|
6890
|
-
},
|
|
6891
|
-
};
|
|
6892
|
-
} catch (e: any) {
|
|
6893
|
-
return { success: false, error: e.message };
|
|
6894
|
-
}
|
|
6895
|
-
}
|
|
6896
|
-
|
|
6897
|
-
case 'cancel_mesh_queue_task': {
|
|
6898
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6899
|
-
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
6900
|
-
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
6901
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
6902
|
-
if (ownerFailure) return ownerFailure;
|
|
6903
|
-
try {
|
|
6904
|
-
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
6905
|
-
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
6906
|
-
const task = cancelTask(meshId, taskId, { reason });
|
|
6907
|
-
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
6908
|
-
return { success: true, task };
|
|
6909
|
-
} catch (e: any) {
|
|
6910
|
-
return { success: false, error: e.message };
|
|
6911
|
-
}
|
|
6912
|
-
}
|
|
6913
|
-
|
|
6914
|
-
case 'requeue_mesh_queue_task': {
|
|
6915
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6916
|
-
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
6917
|
-
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
6918
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
6919
|
-
if (ownerFailure) return ownerFailure;
|
|
6920
|
-
try {
|
|
6921
|
-
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
6922
|
-
const task = requeueTask(meshId, taskId, {
|
|
6923
|
-
reason: typeof args?.reason === 'string' ? args.reason : undefined,
|
|
6924
|
-
targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
|
|
6925
|
-
targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
|
|
6926
|
-
clearTargetNode: args?.clearTargetNode === true,
|
|
6927
|
-
clearTargetSession: args?.clearTargetSession !== false,
|
|
6928
|
-
});
|
|
6929
|
-
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
6930
|
-
return { success: true, task };
|
|
6931
|
-
} catch (e: any) {
|
|
6932
|
-
return { success: false, error: e.message };
|
|
6933
|
-
}
|
|
6934
|
-
}
|
|
6935
|
-
|
|
6936
|
-
case 'add_mesh_node': {
|
|
6937
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6938
|
-
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
6939
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6940
|
-
if (!workspace) return { success: false, error: 'workspace required' };
|
|
6941
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
6942
|
-
if (ownerFailure) return ownerFailure;
|
|
6943
|
-
try {
|
|
6944
|
-
const { addNode } = await import('../config/mesh-config.js');
|
|
6945
|
-
const providerPriority = Array.isArray(args?.providerPriority)
|
|
6946
|
-
? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
|
|
6947
|
-
: [];
|
|
6948
|
-
const readOnly = args?.readOnly === true;
|
|
6949
|
-
const providerRoles = normalizeProviderRoles(args?.providerRoles);
|
|
6950
|
-
const policy = {
|
|
6951
|
-
...(readOnly ? { readOnly: true } : {}),
|
|
6952
|
-
...(providerPriority.length ? { providerPriority } : {}),
|
|
6953
|
-
...(providerRoles.length ? { providerRoles } : {}),
|
|
6954
|
-
};
|
|
6955
|
-
const role = normalizeMeshDaemonRole(args?.role);
|
|
6956
|
-
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
6957
|
-
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
6958
|
-
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
6959
|
-
const node = addNode(meshId, {
|
|
6960
|
-
workspace,
|
|
6961
|
-
...(repoRoot ? { repoRoot } : {}),
|
|
6962
|
-
...(daemonId ? { daemonId } : {}),
|
|
6963
|
-
...(machineId ? { machineId } : {}),
|
|
6964
|
-
...(policy ? { policy } : {}),
|
|
6965
|
-
...(role ? { role } : {}),
|
|
6966
|
-
});
|
|
6967
|
-
if (!node) return { success: false, error: 'Mesh not found' };
|
|
6968
|
-
// mesh_status hands back a coordinator-memory aggregate
|
|
6969
|
-
// snapshot keyed on (meshId, queueRevision). Adding a
|
|
6970
|
-
// node touches neither, so without an explicit cache
|
|
6971
|
-
// bust the dashboard graph keeps rendering the pre-add
|
|
6972
|
-
// node list (empty for a fresh mesh) even after the
|
|
6973
|
-
// user clicks Refresh.
|
|
6974
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
6975
|
-
return { success: true, node };
|
|
6976
|
-
} catch (e: any) {
|
|
6977
|
-
return { success: false, error: e.message };
|
|
6978
|
-
}
|
|
6979
|
-
}
|
|
6980
|
-
|
|
6981
|
-
case 'update_mesh_node': {
|
|
6982
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6983
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
6984
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
6985
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
6986
|
-
if (ownerFailure) return ownerFailure;
|
|
6987
|
-
try {
|
|
6988
|
-
const { updateNode } = await import('../config/mesh-config.js');
|
|
6989
|
-
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
6990
|
-
? { ...(args.policy as Record<string, unknown>) }
|
|
6991
|
-
: {};
|
|
6992
|
-
if (Array.isArray(args?.providerPriority)) {
|
|
6993
|
-
const providerPriority = args.providerPriority
|
|
6994
|
-
.map((type: any) => typeof type === 'string' ? type.trim() : '')
|
|
6995
|
-
.filter(Boolean);
|
|
6996
|
-
delete (policy as any).provider_priority;
|
|
6997
|
-
if (providerPriority.length) {
|
|
6998
|
-
(policy as any).providerPriority = providerPriority;
|
|
6999
|
-
} else {
|
|
7000
|
-
delete (policy as any).providerPriority;
|
|
7001
|
-
}
|
|
7002
|
-
}
|
|
7003
|
-
// providerRoles: per-(node, provider) role label + maxParallel cap.
|
|
7004
|
-
// Passing an explicit (possibly empty) array clears/replaces the
|
|
7005
|
-
// declarations; omitting the arg leaves any value already on policy
|
|
7006
|
-
// untouched (a full policy object passed by the caller still carries it).
|
|
7007
|
-
if (Array.isArray(args?.providerRoles)) {
|
|
7008
|
-
const providerRoles = normalizeProviderRoles(args.providerRoles);
|
|
7009
|
-
if (providerRoles.length) {
|
|
7010
|
-
(policy as any).providerRoles = providerRoles;
|
|
7011
|
-
} else {
|
|
7012
|
-
delete (policy as any).providerRoles;
|
|
7013
|
-
}
|
|
7014
|
-
}
|
|
7015
|
-
const patch: Record<string, unknown> = { policy: policy as any };
|
|
7016
|
-
if (typeof args?.systemPrompt === 'string') {
|
|
7017
|
-
const trimmed = (args.systemPrompt as string).trim();
|
|
7018
|
-
patch.systemPrompt = trimmed || undefined;
|
|
7019
|
-
} else if (args?.systemPrompt === null) {
|
|
7020
|
-
patch.systemPrompt = undefined;
|
|
7021
|
-
}
|
|
7022
|
-
const node = updateNode(meshId, nodeId, patch as any);
|
|
7023
|
-
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
7024
|
-
// Provider priority / systemPrompt changes don't touch
|
|
7025
|
-
// the queue revision, so without a manual bust the
|
|
7026
|
-
// cached aggregate keeps surfacing pre-update values
|
|
7027
|
-
// (priority chip, coordinator prompt preview, etc.).
|
|
7028
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
7029
|
-
return { success: true, node };
|
|
7030
|
-
} catch (e: any) {
|
|
7031
|
-
return { success: false, error: e.message };
|
|
7032
|
-
}
|
|
7033
|
-
}
|
|
7034
|
-
|
|
7035
|
-
case 'cleanup_mesh_sessions': {
|
|
7036
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7037
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7038
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7039
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
7040
|
-
if (ownerFailure) return ownerFailure;
|
|
7041
|
-
try {
|
|
7042
|
-
// preferInline so inline-cache-only clone nodes resolve (matches owner check above).
|
|
7043
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7044
|
-
const mesh = meshRecord?.mesh;
|
|
7045
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7046
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7047
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
7048
|
-
const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
|
|
7049
|
-
const sessionIds = Array.isArray(args?.sessionIds)
|
|
7050
|
-
? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
|
|
7051
|
-
: undefined;
|
|
7052
|
-
const result = await this.cleanupMeshSessions({
|
|
7053
|
-
meshId,
|
|
7054
|
-
nodeId,
|
|
7055
|
-
node,
|
|
7056
|
-
mode,
|
|
7057
|
-
sessionIds,
|
|
7058
|
-
dryRun: args?.dryRun === true,
|
|
7059
|
-
source: 'mesh_cleanup_sessions',
|
|
7060
|
-
});
|
|
7061
|
-
return result;
|
|
7062
|
-
} catch (e: any) {
|
|
7063
|
-
return { success: false, error: e.message };
|
|
7064
|
-
}
|
|
7065
|
-
}
|
|
7066
|
-
|
|
7067
|
-
case 'mesh_init': {
|
|
7068
|
-
const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
|
|
7069
|
-
const mesh = args?.inlineMesh || {};
|
|
7070
|
-
try {
|
|
7071
|
-
const detected = await detectCLIs(this.deps.providerLoader, { includeVersion: true });
|
|
7072
|
-
return { ...runMeshInit(mesh, workspace, detected, {
|
|
7073
|
-
write: args?.write === true,
|
|
7074
|
-
overwrite: args?.overwrite === true,
|
|
7075
|
-
}) };
|
|
7076
|
-
} catch (e: any) {
|
|
7077
|
-
return { success: false, error: e?.message || String(e) };
|
|
7078
|
-
}
|
|
7079
|
-
}
|
|
7080
|
-
|
|
7081
|
-
case 'plan_mesh_refine_node': {
|
|
7082
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7083
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7084
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7085
|
-
// preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
|
|
7086
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7087
|
-
const mesh = meshRecord?.mesh;
|
|
7088
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7089
|
-
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
7090
|
-
return {
|
|
7091
|
-
success: true,
|
|
7092
|
-
dryRun: true,
|
|
7093
|
-
nodeId,
|
|
7094
|
-
workspace: node.workspace,
|
|
7095
|
-
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
7096
|
-
mergeWillRun: false,
|
|
7097
|
-
cleanupWillRun: false,
|
|
7098
|
-
};
|
|
7099
|
-
}
|
|
7100
|
-
|
|
7101
|
-
case 'fast_forward_mesh_node': {
|
|
7102
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7103
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7104
|
-
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
7105
|
-
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
7106
|
-
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
7107
|
-
: undefined;
|
|
7108
|
-
let nodeDaemonId: string | undefined;
|
|
7109
|
-
let allowAutoPublishSubmoduleMainCommits = false;
|
|
7110
|
-
if (meshId && nodeId) {
|
|
7111
|
-
// preferInline so fast-forward can resolve inline-cache-only clone worktree nodes.
|
|
7112
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7113
|
-
const mesh = meshRecord?.mesh;
|
|
7114
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7115
|
-
if (!workspace) {
|
|
7116
|
-
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
7117
|
-
}
|
|
7118
|
-
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
7119
|
-
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
7120
|
-
}
|
|
7121
|
-
allowAutoPublishSubmoduleMainCommits = mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true;
|
|
7122
|
-
nodeDaemonId = typeof node?.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
7123
|
-
}
|
|
7124
|
-
// If the target node belongs to a remote daemon, forward the command there.
|
|
7125
|
-
// _meshDirectDispatch prevents re-forwarding (and P2P self-dial) when the stored
|
|
7126
|
-
// daemonId uses a legacy format that doesn't match the receiving daemon's identity.
|
|
7127
|
-
const selfDaemonId = this.deps.statusInstanceId;
|
|
7128
|
-
// daemonIdsEquivalent: a legacy-form stored daemonId that resolves to THIS
|
|
7129
|
-
// machine's core must be treated as local (not remote) so it is not forwarded /
|
|
7130
|
-
// P2P self-dialed. Equivalent → local.
|
|
7131
|
-
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
7132
|
-
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
7133
|
-
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'fast_forward_mesh_node', {
|
|
7134
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7135
|
-
workspace,
|
|
7136
|
-
_meshDirectDispatch: true,
|
|
7137
|
-
});
|
|
7138
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7139
|
-
}
|
|
7140
|
-
const result = await (fastForwardMeshNode({
|
|
7141
|
-
meshId: meshId || undefined,
|
|
7142
|
-
nodeId: nodeId || undefined,
|
|
7143
|
-
workspace,
|
|
7144
|
-
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
7145
|
-
execute: args?.execute === true,
|
|
7146
|
-
dryRun: args?.dryRun === true,
|
|
7147
|
-
updateSubmodules: args?.updateSubmodules === true,
|
|
7148
|
-
submoduleIgnorePaths,
|
|
7149
|
-
mode: args?.mode === 'push' ? 'push' : 'merge',
|
|
7150
|
-
pushSubmodules: args?.pushSubmodules === true,
|
|
7151
|
-
allowAutoPublishSubmoduleMainCommits,
|
|
7152
|
-
}) as Promise<unknown>);
|
|
7153
|
-
return result as CommandRouterResult;
|
|
7154
|
-
}
|
|
7155
|
-
|
|
7156
|
-
case 'refine_mesh_node': {
|
|
7157
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7158
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7159
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7160
|
-
|
|
7161
|
-
// Remote forward: a worktree node lives on its OWN daemon's machine, so the
|
|
7162
|
-
// refine (cd into node.workspace, merge → push → cleanup) must run on THAT
|
|
7163
|
-
// daemon — not the coordinator, whose filesystem has no such path. The sibling
|
|
7164
|
-
// fast_forward_mesh_node / clone_mesh_node handlers already forward to the
|
|
7165
|
-
// node's daemon; refine_mesh_node was the gap (the coordinator would cd into a
|
|
7166
|
-
// non-existent local path and fail), so remote-machine worktrees could not be
|
|
7167
|
-
// converged at all. Forward both dry-run (plan reads the worktree git state)
|
|
7168
|
-
// and execute (async merge job) so the same machine that owns the worktree
|
|
7169
|
-
// resolves it.
|
|
7170
|
-
//
|
|
7171
|
-
// coordinatorDaemonId: refine is ASYNC — the completed/failed event is queued
|
|
7172
|
-
// on the executing daemon's pending-events queue scoped to a coordinator id and
|
|
7173
|
-
// recovered by the coordinator's reconcile loop (pullRemoteNodeQueues →
|
|
7174
|
-
// get_pending_mesh_events). Without stamping our own status id, the remote
|
|
7175
|
-
// daemon would fall back to ITS OWN statusInstanceId as the coordinator
|
|
7176
|
-
// (startMeshRefineJob), scoping the terminal event to the wrong inbox where the
|
|
7177
|
-
// real coordinator never pulls it. Stamp the canonical status id (which is in
|
|
7178
|
-
// the coordinator's self-identity set used to scope the remote drain) so the
|
|
7179
|
-
// event routes back here. Preserve any caller-supplied coordinatorDaemonId.
|
|
7180
|
-
//
|
|
7181
|
-
// _meshDirectDispatch prevents re-forwarding (and P2P self-dial) once the call
|
|
7182
|
-
// has landed on the owning daemon — that daemon then executes locally even if
|
|
7183
|
-
// the stored daemonId uses a legacy form that doesn't match its own identity.
|
|
7184
|
-
{
|
|
7185
|
-
const meshRecordForForward = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7186
|
-
const forwardNode = meshRecordForForward?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7187
|
-
const nodeDaemonId = typeof forwardNode?.daemonId === 'string' ? forwardNode.daemonId.trim() : undefined;
|
|
7188
|
-
const selfDaemonId = this.deps.statusInstanceId;
|
|
7189
|
-
// daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core
|
|
7190
|
-
// is local — execute locally instead of forwarding. Equivalent → local.
|
|
7191
|
-
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
7192
|
-
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
7193
|
-
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
7194
|
-
? args.coordinatorDaemonId.trim()
|
|
7195
|
-
: undefined;
|
|
7196
|
-
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'refine_mesh_node', {
|
|
7197
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7198
|
-
coordinatorDaemonId: callerCoordinatorDaemonId || selfDaemonId,
|
|
7199
|
-
_meshDirectDispatch: true,
|
|
7200
|
-
});
|
|
7201
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7202
|
-
}
|
|
7203
|
-
}
|
|
7204
|
-
|
|
7205
|
-
// Dry-run (plan-only) is the default and stays synchronous: it does no
|
|
7206
|
-
// validation/merge/push and returns the plan instantly. Only execute=true
|
|
7207
|
-
// (and not dry_run) goes through the async refine job that actually
|
|
7208
|
-
// validates → merges → pushes → cleans up. Mirrors the
|
|
7209
|
-
// batch_refine_mesh_nodes / fast_forward_mesh_node dry_run/execute contract.
|
|
7210
|
-
const isDryRun = args?.dryRun !== false && args?.execute !== true;
|
|
7211
|
-
if (isDryRun) {
|
|
7212
|
-
// preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
|
|
7213
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7214
|
-
const mesh = meshRecord?.mesh;
|
|
7215
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7216
|
-
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
7217
|
-
return {
|
|
7218
|
-
success: true,
|
|
7219
|
-
dryRun: true,
|
|
7220
|
-
nodeId,
|
|
7221
|
-
workspace: node.workspace,
|
|
7222
|
-
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
7223
|
-
mergeWillRun: false,
|
|
7224
|
-
cleanupWillRun: false,
|
|
7225
|
-
hint: 'Dry-run only — no merge/push/cleanup performed. Re-invoke with execute:true to converge this node.',
|
|
7226
|
-
};
|
|
7227
|
-
}
|
|
7228
|
-
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
7229
|
-
}
|
|
7230
|
-
|
|
7231
|
-
case 'batch_refine_mesh_nodes': {
|
|
7232
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7233
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7234
|
-
const requestedNodeIds = Array.isArray(args?.nodeIds)
|
|
7235
|
-
? (args.nodeIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
|
|
7236
|
-
: undefined;
|
|
7237
|
-
// Dry-run (plan-only) stays synchronous: it does no validation/merge and
|
|
7238
|
-
// returns instantly. Execute goes through the async batch job — immediate
|
|
7239
|
-
// {async:true, status:'accepted'} + background convergence + terminal event,
|
|
7240
|
-
// matching the single-node refine_mesh_node contract so long validation
|
|
7241
|
-
// suites can't time out the IPC and strand the coordinator.
|
|
7242
|
-
const isDryRun = args?.dryRun !== false && args?.execute !== true;
|
|
7243
|
-
if (isDryRun) return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
|
|
7244
|
-
return this.startMeshRefineBatchJob(meshId, requestedNodeIds, args);
|
|
7245
|
-
}
|
|
7246
|
-
|
|
7247
|
-
case 'remove_mesh_node': {
|
|
7248
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7249
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7250
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
7251
|
-
try {
|
|
7252
|
-
// preferInline so removal can resolve inline-cache-only clone worktree nodes.
|
|
7253
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7254
|
-
const mesh = meshRecord?.mesh;
|
|
7255
|
-
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7256
|
-
|
|
7257
|
-
// Guard: refuse to remove the coordinator's OWN local base node
|
|
7258
|
-
// (same machine, NOT a worktree). Removing it breaks live mesh
|
|
7259
|
-
// membership — the coordinator can no longer be reached and has
|
|
7260
|
-
// to be restarted. Worktree clones are always safe to remove;
|
|
7261
|
-
// only the non-worktree node bound to this daemon is protected.
|
|
7262
|
-
// An explicit force:true overrides for intentional mesh teardown.
|
|
7263
|
-
if (node && !args?._meshDirectDispatch && node.isLocalWorktree !== true && args?.force !== true) {
|
|
7264
|
-
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : '';
|
|
7265
|
-
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>) || '';
|
|
7266
|
-
const selfDaemonId = this.deps.statusInstanceId || '';
|
|
7267
|
-
const selfMachineId = (() => { try { return loadConfig().machineId || ''; } catch { return ''; } })();
|
|
7268
|
-
const isCoordinatorBaseNode =
|
|
7269
|
-
(!!selfDaemonId && (nodeDaemonId === selfDaemonId || nodeMachineId === selfDaemonId))
|
|
7270
|
-
|| (!!selfMachineId && (nodeDaemonId === selfMachineId || nodeMachineId === selfMachineId));
|
|
7271
|
-
if (isCoordinatorBaseNode) {
|
|
7272
|
-
return {
|
|
7273
|
-
success: false,
|
|
7274
|
-
removed: false,
|
|
7275
|
-
code: 'mesh_remove_coordinator_base_node_protected',
|
|
7276
|
-
error: `Refusing to remove the coordinator's own base node '${typeof node.workspace === 'string' ? node.workspace : nodeId}'. `
|
|
7277
|
-
+ `It is the local non-worktree node bound to this coordinator daemon; removing it breaks live mesh membership and forces a restart.`,
|
|
7278
|
-
recoveryHint: 'Remove worktree clone nodes instead, or pass force:true only if you are intentionally tearing down this mesh and accept that the coordinator must be re-registered/restarted.',
|
|
7279
|
-
};
|
|
7280
|
-
}
|
|
7281
|
-
}
|
|
7282
|
-
|
|
7283
|
-
const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
|
|
7284
|
-
args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
|
|
7285
|
-
);
|
|
7286
|
-
// Explicit sessionIds (e.g. supplied by refine auto-cleanup) bypass the
|
|
7287
|
-
// workspace-only-match guard so a delegate session that lacks a
|
|
7288
|
-
// meta.meshNodeId binding can still be stopped/deleted.
|
|
7289
|
-
const explicitSessionIds = Array.isArray(args?.sessionIds)
|
|
7290
|
-
? (args.sessionIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
|
|
7291
|
-
: undefined;
|
|
7292
|
-
let sessionCleanup: Record<string, unknown> | undefined;
|
|
7293
|
-
if (node && sessionCleanupMode !== 'preserve') {
|
|
7294
|
-
sessionCleanup = await this.cleanupMeshSessions({
|
|
7295
|
-
meshId,
|
|
7296
|
-
nodeId,
|
|
7297
|
-
node,
|
|
7298
|
-
mode: sessionCleanupMode,
|
|
7299
|
-
...(explicitSessionIds && explicitSessionIds.length > 0 ? { sessionIds: explicitSessionIds } : {}),
|
|
7300
|
-
source: 'mesh_remove_node',
|
|
7301
|
-
});
|
|
7302
|
-
if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
|
|
7303
|
-
}
|
|
7304
|
-
|
|
7305
|
-
let worktreeCleanup: Record<string, unknown> | undefined;
|
|
7306
|
-
if (node?.isLocalWorktree) {
|
|
7307
|
-
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
7308
|
-
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
7309
|
-
// clean up locally, do not forward. Equivalent → local.
|
|
7310
|
-
const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
7311
|
-
&& !args?._meshDirectDispatch;
|
|
7312
|
-
if (isRemoteWorktree) {
|
|
7313
|
-
// Worktree lives on a different machine — ask that daemon to clean it up.
|
|
7314
|
-
// _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
|
|
7315
|
-
const forwarded = await this.deps.dispatchMeshCommand!(nodeDaemonId!, 'remove_mesh_node', {
|
|
7316
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7317
|
-
_meshDirectDispatch: true,
|
|
7318
|
-
});
|
|
7319
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7320
|
-
}
|
|
7321
|
-
const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId, force: args?.force === true });
|
|
7322
|
-
// De-gating: membership removal is NOT gated on the worktree
|
|
7323
|
-
// directory actually being deleted. cleanupLocalWorktreeNode now
|
|
7324
|
-
// returns success:true (with a residue flag) whenever the path is
|
|
7325
|
-
// proven managed and the only remaining problem is leftover
|
|
7326
|
-
// directory bytes (e.g. Windows EINVAL). A success:false here means
|
|
7327
|
-
// a genuinely-unsafe condition — missing metadata, a non-managed /
|
|
7328
|
-
// unexpected path, a branch mismatch, a dirty worktree, or an
|
|
7329
|
-
// unverified force fallback — and those still block removal.
|
|
7330
|
-
if (cleanupResult.success === false) {
|
|
7331
|
-
return {
|
|
7332
|
-
success: false,
|
|
7333
|
-
removed: false,
|
|
7334
|
-
code: cleanupResult.code,
|
|
7335
|
-
error: cleanupResult.error,
|
|
7336
|
-
recoveryHint: cleanupResult.recoveryHint,
|
|
7337
|
-
...(sessionCleanup ? { sessionCleanup } : {}),
|
|
7338
|
-
worktreeCleanup: cleanupResult,
|
|
7339
|
-
};
|
|
7340
|
-
}
|
|
7341
|
-
worktreeCleanup = cleanupResult;
|
|
7342
|
-
}
|
|
7343
|
-
|
|
7344
|
-
let removed = false;
|
|
7345
|
-
if (meshRecord?.inline) {
|
|
7346
|
-
removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
|
|
7347
|
-
// Inline meshes share the same aggregate snapshot cache as
|
|
7348
|
-
// local-config meshes; without this bust the removed node
|
|
7349
|
-
// keeps showing up in the dashboard graph until the cache
|
|
7350
|
-
// ages out on its own.
|
|
7351
|
-
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
7352
|
-
// Node was already absent from the inline mesh (e.g. removed by a
|
|
7353
|
-
// prior refine cleanup). Treat as removed so caller gets removed:true.
|
|
7354
|
-
if (!removed && !node) removed = true;
|
|
7355
|
-
} else {
|
|
7356
|
-
const { removeNode } = await import('../config/mesh-config.js');
|
|
7357
|
-
removed = removeNode(meshId, nodeId);
|
|
7358
|
-
// Node already absent from config (e.g. removed by a prior refine
|
|
7359
|
-
// cleanup after a successful Refinery merge). Treat as removed so
|
|
7360
|
-
// the response is accurate.
|
|
7361
|
-
if (!removed && !node) removed = true;
|
|
7362
|
-
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
7363
|
-
}
|
|
7364
|
-
|
|
7365
|
-
// Record in task ledger
|
|
7366
|
-
if (removed) {
|
|
7367
|
-
try {
|
|
7368
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
7369
|
-
appendLedgerEntry(meshId, {
|
|
7370
|
-
kind: 'node_removed',
|
|
7371
|
-
nodeId,
|
|
7372
|
-
payload: {
|
|
7373
|
-
worktree: !!node?.isLocalWorktree,
|
|
7374
|
-
sessionCleanupMode,
|
|
7375
|
-
workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
|
|
7376
|
-
daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
|
|
7377
|
-
worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
|
|
7378
|
-
worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
|
|
7379
|
-
forced: worktreeCleanup?.forced === true ? true : undefined,
|
|
7380
|
-
forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
|
|
7381
|
-
},
|
|
7382
|
-
});
|
|
7383
|
-
} catch { /* ledger append is best-effort */ }
|
|
7384
|
-
}
|
|
7385
|
-
|
|
7386
|
-
// Surface leftover-directory residue at the top level so callers
|
|
7387
|
-
// see the node was dropped from the mesh even though the worktree
|
|
7388
|
-
// directory could not be fully removed (best-effort, non-gating).
|
|
7389
|
-
const residueWarning = worktreeCleanup?.residue === true && typeof worktreeCleanup?.residueWarning === 'string'
|
|
7390
|
-
? worktreeCleanup.residueWarning
|
|
7391
|
-
: undefined;
|
|
7392
|
-
return {
|
|
7393
|
-
success: true,
|
|
7394
|
-
removed,
|
|
7395
|
-
...(residueWarning ? { residueWarning } : {}),
|
|
7396
|
-
...(sessionCleanup ? { sessionCleanup } : {}),
|
|
7397
|
-
...(worktreeCleanup ? { worktreeCleanup } : {}),
|
|
7398
|
-
};
|
|
7399
|
-
} catch (e: any) {
|
|
7400
|
-
return { success: false, error: e.message };
|
|
7401
|
-
}
|
|
7402
|
-
}
|
|
7403
|
-
|
|
7404
|
-
case 'clone_mesh_node': {
|
|
7405
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7406
|
-
const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
|
|
7407
|
-
const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
|
|
7408
|
-
const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
|
|
7409
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7410
|
-
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
7411
|
-
if (!branch) return { success: false, error: 'branch required' };
|
|
7412
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
7413
|
-
if (ownerFailure) return ownerFailure;
|
|
7414
|
-
|
|
7415
|
-
try {
|
|
7416
|
-
// Resolve with preferInline so the clone writes the new node into the
|
|
7417
|
-
// same representation that get_mesh reads back. The MCP coordinator
|
|
7418
|
-
// passes inlineMesh on every mesh command, so when it owns an inline
|
|
7419
|
-
// mesh the membership read path (get_mesh, preferInline: true) returns
|
|
7420
|
-
// the inline cache. Without preferInline here, clone could resolve to a
|
|
7421
|
-
// local-config mesh and write the node only to config — leaving the
|
|
7422
|
-
// inline cache (and therefore get_mesh / refreshMeshFromDaemon) without
|
|
7423
|
-
// the node, so the new worktree node is never visible in live mesh
|
|
7424
|
-
// membership even though worktree_bootstrap_complete fires.
|
|
7425
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7426
|
-
const mesh = meshRecord?.mesh;
|
|
7427
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7428
|
-
|
|
7429
|
-
const sourceNode = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, sourceNodeId));
|
|
7430
|
-
if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
|
|
7431
|
-
|
|
7432
|
-
// Forward to the source node's daemon if it's on a different machine.
|
|
7433
|
-
// _meshDirectDispatch prevents infinite re-forwarding when the stored daemonId
|
|
7434
|
-
// uses a legacy format that doesn't match the receiving daemon's statusInstanceId.
|
|
7435
|
-
const sourceDaemonId = typeof sourceNode.daemonId === 'string' ? sourceNode.daemonId.trim() : undefined;
|
|
7436
|
-
// daemonIdsEquivalent: an equivalent-form source daemonId is this machine —
|
|
7437
|
-
// clone locally, do not forward. Equivalent → local.
|
|
7438
|
-
if (sourceDaemonId && !daemonIdsEquivalent(sourceDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
7439
|
-
&& !args?._meshDirectDispatch) {
|
|
7440
|
-
const forwarded = await this.deps.dispatchMeshCommand(sourceDaemonId, 'clone_mesh_node', {
|
|
7441
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7442
|
-
_meshDirectDispatch: true,
|
|
7443
|
-
});
|
|
7444
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7445
|
-
}
|
|
7446
|
-
|
|
7447
|
-
const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
|
|
7448
|
-
const { createWorktree } = await import('../git/git-worktree.js');
|
|
7449
|
-
const result = await createWorktree({
|
|
7450
|
-
repoRoot,
|
|
7451
|
-
branch,
|
|
7452
|
-
baseBranch,
|
|
7453
|
-
meshName: mesh.name,
|
|
7454
|
-
});
|
|
7455
|
-
|
|
7456
|
-
let node: any;
|
|
7457
|
-
if (meshRecord.inline) {
|
|
7458
|
-
const { randomUUID } = await import('crypto');
|
|
7459
|
-
node = {
|
|
7460
|
-
id: `node_${randomUUID().replace(/-/g, '')}`,
|
|
7461
|
-
workspace: result.worktreePath,
|
|
7462
|
-
repoRoot: result.worktreePath,
|
|
7463
|
-
daemonId: sourceNode.daemonId,
|
|
7464
|
-
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
7465
|
-
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
7466
|
-
policy: { ...(sourceNode.policy || {}) },
|
|
7467
|
-
isLocalWorktree: true,
|
|
7468
|
-
worktreeBranch: result.branch,
|
|
7469
|
-
clonedFromNodeId: sourceNodeId,
|
|
7470
|
-
};
|
|
7471
|
-
this.updateInlineMeshNode(meshId, mesh, node);
|
|
7472
|
-
} else {
|
|
7473
|
-
const { addNode } = await import('../config/mesh-config.js');
|
|
7474
|
-
node = addNode(meshId, {
|
|
7475
|
-
workspace: result.worktreePath,
|
|
7476
|
-
repoRoot: result.worktreePath,
|
|
7477
|
-
daemonId: sourceNode.daemonId,
|
|
7478
|
-
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
7479
|
-
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
7480
|
-
isLocalWorktree: true,
|
|
7481
|
-
worktreeBranch: result.branch,
|
|
7482
|
-
clonedFromNodeId: sourceNodeId,
|
|
7483
|
-
policy: { ...(sourceNode.policy || {}) },
|
|
7484
|
-
});
|
|
7485
|
-
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
7486
|
-
// Also reconcile the freshly-registered node into any warmed inline
|
|
7487
|
-
// cache for this mesh. get_mesh (preferInline: true) reads the inline
|
|
7488
|
-
// cache first when one exists; if we only wrote to local config the
|
|
7489
|
-
// node would be invisible to membership reads. updateInlineMeshNode is
|
|
7490
|
-
// a no-op when no inline cache is present.
|
|
7491
|
-
const inlineForReconcile = this.getCachedInlineMesh(meshId);
|
|
7492
|
-
if (inlineForReconcile) this.updateInlineMeshNode(meshId, inlineForReconcile, node);
|
|
7493
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
7494
|
-
}
|
|
7495
|
-
|
|
7496
|
-
const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
7497
|
-
node.worktreeBootstrap = bootstrapState;
|
|
7498
|
-
if (meshRecord.inline) {
|
|
7499
|
-
this.updateInlineMeshNode(meshId, mesh, node);
|
|
7500
|
-
return;
|
|
7501
|
-
}
|
|
7502
|
-
try {
|
|
7503
|
-
const { updateNode } = await import('../config/mesh-config.js');
|
|
7504
|
-
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
7505
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
7506
|
-
} catch { /* bootstrap status persistence is best-effort */ }
|
|
7507
|
-
};
|
|
7508
|
-
|
|
7509
|
-
const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
7510
|
-
try {
|
|
7511
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
7512
|
-
appendLedgerEntry(meshId, {
|
|
7513
|
-
kind: 'node_cloned',
|
|
7514
|
-
nodeId: node.id,
|
|
7515
|
-
payload: {
|
|
7516
|
-
sourceNodeId,
|
|
7517
|
-
branch: result.branch,
|
|
7518
|
-
worktreePath: result.worktreePath,
|
|
7519
|
-
submodulesInitialized: initSubmodules,
|
|
7520
|
-
worktreeBootstrap: {
|
|
7521
|
-
status: bootstrapState.status,
|
|
7522
|
-
required: bootstrapState.required,
|
|
7523
|
-
configSource: bootstrapState.configSource,
|
|
7524
|
-
configSourceType: bootstrapState.configSourceType,
|
|
7525
|
-
lastCommand: bootstrapState.lastCommand,
|
|
7526
|
-
exitCode: bootstrapState.exitCode,
|
|
7527
|
-
},
|
|
7528
|
-
},
|
|
7529
|
-
});
|
|
7530
|
-
} catch { /* ledger append is best-effort */ }
|
|
7531
|
-
};
|
|
7532
|
-
|
|
7533
|
-
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
7534
|
-
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
|
|
7535
|
-
const runningBootstrapState: WorktreeBootstrapState = {
|
|
7536
|
-
status: 'running',
|
|
7537
|
-
required: loadedBootstrap.config?.required !== false,
|
|
7538
|
-
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
7539
|
-
configSourceType: loadedBootstrap.sourceType,
|
|
7540
|
-
startedAt: new Date().toISOString(),
|
|
7541
|
-
};
|
|
7542
|
-
await persistWorktreeSetupState(runningBootstrapState);
|
|
7543
|
-
|
|
7544
|
-
const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
|
|
7545
|
-
let submodulesInitialized = false;
|
|
7546
|
-
if (initSubmodules) {
|
|
7547
|
-
try {
|
|
7548
|
-
const { runGit } = await import('../git/git-executor.js');
|
|
7549
|
-
await runGit(
|
|
7550
|
-
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
7551
|
-
['submodule', 'update', '--init', '--recursive'],
|
|
7552
|
-
{ timeoutMs: 120000 },
|
|
7553
|
-
);
|
|
7554
|
-
submodulesInitialized = true;
|
|
7555
|
-
|
|
7556
|
-
// Sync oss submodule to source node HEAD (best-effort)
|
|
7557
|
-
const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
|
|
7558
|
-
if (sourceWorkspace) {
|
|
7559
|
-
try {
|
|
7560
|
-
const { runGit: rg } = await import('../git/git-executor.js');
|
|
7561
|
-
const sourceCtx = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
|
|
7562
|
-
const worktreeCtx = { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true };
|
|
7563
|
-
|
|
7564
|
-
// Read source node's oss submodule SHA
|
|
7565
|
-
const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', 'oss'], { timeoutMs: 10000 });
|
|
7566
|
-
const sourceStatusLine = (typeof sourceStatusOut === 'string' ? sourceStatusOut : (sourceStatusOut as any)?.stdout ?? '').trim();
|
|
7567
|
-
const sourceShaMatch = sourceStatusLine.match(/^[+\- ]?([0-9a-f]{40})/);
|
|
7568
|
-
const sourceSha = sourceShaMatch?.[1];
|
|
7569
|
-
|
|
7570
|
-
if (sourceSha) {
|
|
7571
|
-
// Read worktree's current oss HEAD
|
|
7572
|
-
const ossCtx = { workspace: `${result.worktreePath}/oss`, repoRoot: `${result.worktreePath}/oss`, isGitRepo: true };
|
|
7573
|
-
const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
|
|
7574
|
-
const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
|
|
7575
|
-
|
|
7576
|
-
if (worktreeOssSha !== sourceSha) {
|
|
7577
|
-
// Fetch target SHA from source node's oss directory
|
|
7578
|
-
await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
|
|
7579
|
-
await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
|
|
7580
|
-
await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
|
|
7581
|
-
await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
|
|
7582
|
-
console.log(`[mesh] Synced oss submodule to source HEAD ${sourceSha.slice(0, 8)} in worktree`);
|
|
7583
|
-
}
|
|
7584
|
-
}
|
|
7585
|
-
} catch (ossErr: any) {
|
|
7586
|
-
console.warn('[mesh] oss submodule sync to source HEAD failed (best-effort):', ossErr.message);
|
|
7587
|
-
}
|
|
7588
|
-
}
|
|
7589
|
-
} catch (subErr: any) {
|
|
7590
|
-
// Submodule init is best-effort; don't fail the clone
|
|
7591
|
-
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
7592
|
-
}
|
|
7593
|
-
}
|
|
7594
|
-
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
7595
|
-
await persistWorktreeSetupState(bootstrapState);
|
|
7596
|
-
await appendCloneLedger(submodulesInitialized, bootstrapState);
|
|
7597
|
-
return { submodulesInitialized, bootstrapState };
|
|
7598
|
-
};
|
|
7599
|
-
|
|
7600
|
-
const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
|
|
7601
|
-
const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
|
|
7602
|
-
? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
|
|
7603
|
-
: 8000;
|
|
7604
|
-
const setupPromise = finishWorktreeSetup();
|
|
7605
|
-
const setupResult = await Promise.race([
|
|
7606
|
-
setupPromise.then((value) => ({ completed: true as const, value })),
|
|
7607
|
-
new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
|
|
7608
|
-
]);
|
|
7609
|
-
|
|
7610
|
-
const emitBootstrapEvent = (eventStatus: 'bootstrap_complete' | 'bootstrap_failed', bootstrapState: WorktreeBootstrapState, startedAtMs: number, extraPayload?: Record<string, unknown>): void => {
|
|
7611
|
-
try {
|
|
7612
|
-
const durationMs = Date.now() - startedAtMs;
|
|
7613
|
-
const event = `worktree_${eventStatus}` as const;
|
|
7614
|
-
const metadataEvent = {
|
|
7615
|
-
source: 'clone_mesh_node_bootstrap',
|
|
7616
|
-
nodeId: node.id,
|
|
7617
|
-
status: eventStatus,
|
|
7618
|
-
worktreePath: result.worktreePath,
|
|
7619
|
-
durationMs,
|
|
7620
|
-
bootstrapStatus: bootstrapState.status,
|
|
7621
|
-
...(bootstrapState.error ? { error: bootstrapState.error } : {}),
|
|
7622
|
-
...(bootstrapState.exitCode !== undefined ? { exitCode: bootstrapState.exitCode } : {}),
|
|
7623
|
-
...(extraPayload || {}),
|
|
7624
|
-
};
|
|
7625
|
-
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
7626
|
-
const forwarded = handleMeshForwardEvent(
|
|
7627
|
-
{ instanceManager: this.deps.instanceManager } as any,
|
|
7628
|
-
{ event, meshId, nodeId: node.id, workspace: result.worktreePath, metadataEvent },
|
|
7629
|
-
);
|
|
7630
|
-
if (forwarded?.success === true) return;
|
|
7631
|
-
}
|
|
7632
|
-
queuePendingMeshCoordinatorEvent({
|
|
7633
|
-
event,
|
|
7634
|
-
meshId,
|
|
7635
|
-
nodeLabel: node.id,
|
|
7636
|
-
nodeId: node.id,
|
|
7637
|
-
workspace: result.worktreePath,
|
|
7638
|
-
metadataEvent,
|
|
7639
|
-
queuedAt: Date.now(),
|
|
7640
|
-
});
|
|
7641
|
-
} catch { /* event emission is best-effort */ }
|
|
7642
|
-
};
|
|
7643
|
-
|
|
7644
|
-
const bootstrapStartedMs = Date.now();
|
|
7645
|
-
|
|
7646
|
-
if (!setupResult.completed) {
|
|
7647
|
-
setupPromise
|
|
7648
|
-
.then(({ bootstrapState }) => {
|
|
7649
|
-
emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
|
|
7650
|
-
})
|
|
7651
|
-
.catch((error: any) => {
|
|
7652
|
-
const failedState: WorktreeBootstrapState = {
|
|
7653
|
-
...runningBootstrapState,
|
|
7654
|
-
status: 'failed',
|
|
7655
|
-
completedAt: new Date().toISOString(),
|
|
7656
|
-
error: error?.message || String(error),
|
|
7657
|
-
};
|
|
7658
|
-
void persistWorktreeSetupState(failedState);
|
|
7659
|
-
void appendCloneLedger(false, failedState);
|
|
7660
|
-
emitBootstrapEvent('bootstrap_failed', failedState, bootstrapStartedMs, { error: error?.message || String(error) });
|
|
7661
|
-
});
|
|
7662
|
-
return {
|
|
7663
|
-
success: true,
|
|
7664
|
-
async: true,
|
|
7665
|
-
status: 'accepted',
|
|
7666
|
-
node,
|
|
7667
|
-
worktreePath: result.worktreePath,
|
|
7668
|
-
branch: result.branch,
|
|
7669
|
-
worktreeBootstrap: runningBootstrapState,
|
|
7670
|
-
worktreeSetup: {
|
|
7671
|
-
status: 'running',
|
|
7672
|
-
setupWaitMs,
|
|
7673
|
-
message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
|
|
7674
|
-
},
|
|
7675
|
-
};
|
|
7676
|
-
}
|
|
7677
|
-
|
|
7678
|
-
const { submodulesInitialized, bootstrapState } = setupResult.value;
|
|
7679
|
-
emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
|
|
7680
|
-
return {
|
|
7681
|
-
success: true,
|
|
7682
|
-
node,
|
|
7683
|
-
worktreePath: result.worktreePath,
|
|
7684
|
-
branch: result.branch,
|
|
7685
|
-
submodulesInitialized,
|
|
7686
|
-
worktreeBootstrap: bootstrapState,
|
|
7687
|
-
};
|
|
7688
|
-
} catch (e: any) {
|
|
7689
|
-
return { success: false, error: e.message };
|
|
7690
|
-
}
|
|
7691
|
-
}
|
|
7692
|
-
case 'retry_mesh_node_bootstrap': {
|
|
7693
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7694
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
7695
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7696
|
-
if (!nodeId) return { success: false, error: 'nodeId required' };
|
|
7697
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'bootstrap retry');
|
|
7698
|
-
if (ownerFailure) return ownerFailure;
|
|
7699
|
-
|
|
7700
|
-
try {
|
|
7701
|
-
// preferInline so bootstrap-retry can resolve inline-cache-only clone worktree nodes.
|
|
7702
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
7703
|
-
const mesh = meshRecord?.mesh;
|
|
7704
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7705
|
-
|
|
7706
|
-
const node = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7707
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
7708
|
-
if (!node.isLocalWorktree) return { success: false, error: 'Node is not a local worktree node' };
|
|
7709
|
-
|
|
7710
|
-
// Bootstrap runs scripts in the worktree path — forward to the node's daemon if remote.
|
|
7711
|
-
// _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
|
|
7712
|
-
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
7713
|
-
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
7714
|
-
// bootstrap locally, do not forward. Equivalent → local.
|
|
7715
|
-
if (nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
7716
|
-
&& !args?._meshDirectDispatch) {
|
|
7717
|
-
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId, 'retry_mesh_node_bootstrap', {
|
|
7718
|
-
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
7719
|
-
_meshDirectDispatch: true,
|
|
7720
|
-
});
|
|
7721
|
-
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
7722
|
-
}
|
|
7723
|
-
|
|
7724
|
-
const currentBootstrap = node.worktreeBootstrap as WorktreeBootstrapState | undefined;
|
|
7725
|
-
if (currentBootstrap?.status === 'running') {
|
|
7726
|
-
return { success: false, error: 'Bootstrap is already running for this node' };
|
|
7727
|
-
}
|
|
7728
|
-
|
|
7729
|
-
const worktreePath: string = node.workspace || node.repoRoot;
|
|
7730
|
-
if (!worktreePath) return { success: false, error: 'Node has no workspace path' };
|
|
7731
|
-
|
|
7732
|
-
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, worktreePath);
|
|
7733
|
-
const runningState: WorktreeBootstrapState = {
|
|
7734
|
-
status: 'running',
|
|
7735
|
-
required: loadedBootstrap.config?.required !== false,
|
|
7736
|
-
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
7737
|
-
configSourceType: loadedBootstrap.sourceType,
|
|
7738
|
-
startedAt: new Date().toISOString(),
|
|
7739
|
-
};
|
|
7740
|
-
|
|
7741
|
-
const persistState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
7742
|
-
node.worktreeBootstrap = bootstrapState;
|
|
7743
|
-
if (meshRecord.inline) {
|
|
7744
|
-
this.updateInlineMeshNode(meshId, mesh, node);
|
|
7745
|
-
return;
|
|
7746
|
-
}
|
|
7747
|
-
try {
|
|
7748
|
-
const { updateNode } = await import('../config/mesh-config.js');
|
|
7749
|
-
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
7750
|
-
this.invalidateAggregateMeshStatus(meshId);
|
|
7751
|
-
} catch { /* best-effort */ }
|
|
7752
|
-
};
|
|
7753
|
-
|
|
7754
|
-
await persistState(runningState);
|
|
7755
|
-
const bootstrapState = await runMeshWorktreeBootstrap(mesh, worktreePath);
|
|
7756
|
-
await persistState(bootstrapState);
|
|
7757
|
-
|
|
7758
|
-
return { success: true, bootstrapState };
|
|
7759
|
-
} catch (e: any) {
|
|
7760
|
-
return { success: false, error: e.message };
|
|
7761
|
-
}
|
|
7762
|
-
}
|
|
7763
|
-
|
|
7764
|
-
case 'trigger_mesh_queue': {
|
|
7765
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
7766
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
7767
|
-
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
7768
|
-
if (ownerFailure) return ownerFailure;
|
|
7769
|
-
try {
|
|
7770
|
-
const { triggerMeshQueue, tryAssignQueueTask } = await import('../mesh/mesh-events.js');
|
|
7771
|
-
|
|
7772
|
-
// Bug A fix: when preferredNodeId is provided, attempt to claim a pending
|
|
7773
|
-
// task for the preferred node's idle session first, before the general
|
|
7774
|
-
// round-robin trigger picks a different node.
|
|
7775
|
-
const preferredNodeId = typeof args?.preferredNodeId === 'string' ? args.preferredNodeId.trim() : '';
|
|
7776
|
-
if (preferredNodeId) {
|
|
7777
|
-
const cliInstances = this.deps.instanceManager.getByCategory('cli');
|
|
7778
|
-
// Sort: preferred node's sessions first, others after
|
|
7779
|
-
const sorted = [...cliInstances].sort((a, b) => {
|
|
7780
|
-
const aSettings = a.getState().settings as Record<string, unknown> || {};
|
|
7781
|
-
const bSettings = b.getState().settings as Record<string, unknown> || {};
|
|
7782
|
-
const aNode = readStringValue(aSettings.meshNodeId, aSettings.nodeId);
|
|
7783
|
-
const bNode = readStringValue(bSettings.meshNodeId, bSettings.nodeId);
|
|
7784
|
-
return (aNode === preferredNodeId ? -1 : 0) - (bNode === preferredNodeId ? -1 : 0);
|
|
7785
|
-
});
|
|
7786
|
-
for (const inst of sorted) {
|
|
7787
|
-
const state = inst.getState();
|
|
7788
|
-
const settings = state.settings as Record<string, unknown> || {};
|
|
7789
|
-
const nodeId = readStringValue(settings.meshNodeId, settings.nodeId);
|
|
7790
|
-
if (!nodeId || nodeId !== preferredNodeId) continue;
|
|
7791
|
-
const meshNodeFor = readStringValue(settings.meshNodeFor);
|
|
7792
|
-
if (meshNodeFor !== meshId) continue;
|
|
7793
|
-
const status = (readStringValue(state.status) || '').toLowerCase();
|
|
7794
|
-
if (status !== 'idle') continue;
|
|
7795
|
-
const sessionId = typeof state.instanceId === 'string' ? state.instanceId : '';
|
|
7796
|
-
const providerType = readStringValue(state.type, settings.providerType) || '';
|
|
7797
|
-
if (sessionId && providerType) {
|
|
7798
|
-
tryAssignQueueTask(this.deps as any, meshId, nodeId, sessionId, providerType);
|
|
7799
|
-
break;
|
|
7800
|
-
}
|
|
7801
|
-
}
|
|
7802
|
-
}
|
|
7803
|
-
|
|
7804
|
-
const trigger = await triggerMeshQueue(this.deps as any, meshId);
|
|
7805
|
-
return { success: true, trigger };
|
|
7806
|
-
} catch (e: any) {
|
|
7807
|
-
return { success: false, error: e.message };
|
|
7808
|
-
}
|
|
7809
|
-
}
|
|
7810
|
-
|
|
7811
6266
|
// ─── Mesh Coordinator Launch ───
|
|
7812
6267
|
case 'launch_mesh_coordinator': {
|
|
7813
6268
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|