@adhdev/daemon-core 0.9.82-rc.17 → 0.9.82-rc.171
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +22 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +72 -1
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +7 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +71 -1
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +35 -6
- package/dist/index.js +18634 -3760
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18538 -3732
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +72 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +30 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +96 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +44 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/cli-provider-instance.d.ts +9 -3
- package/dist/providers/contracts.d.ts +98 -5
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +24 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance.d.ts +3 -0
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +56 -0
- package/dist/providers/spec/cli-adapter.d.ts +95 -0
- package/dist/providers/spec/driver.d.ts +149 -0
- package/dist/providers/spec/evaluator.d.ts +47 -0
- package/dist/providers/spec/loader.d.ts +14 -0
- package/dist/providers/spec/native-history-executor.d.ts +39 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +511 -0
- package/dist/providers/spec/types.d.ts +212 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/repo-mesh-types.d.ts +100 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -2
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +17 -10
- package/src/boot/process-hardening.ts +89 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +14 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +21 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +59 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +1597 -60
- package/src/commands/cli-manager.ts +214 -3
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +231 -127
- package/src/commands/router.ts +3574 -502
- package/src/config/chat-history.ts +87 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +264 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +17 -5
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +113 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +479 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +204 -30
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +879 -61
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +216 -158
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +456 -54
- package/src/providers/contracts.ts +105 -5
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/dispatcher.ts +227 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance.ts +3 -0
- package/src/providers/provider-loader.ts +585 -50
- package/src/providers/provider-schema.ts +31 -13
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +210 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +168 -0
- package/src/providers/spec/cli-adapter.ts +430 -0
- package/src/providers/spec/driver.ts +502 -0
- package/src/providers/spec/evaluator.ts +298 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +623 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +511 -0
- package/src/providers/spec/schema.json +211 -0
- package/src/providers/spec/types.ts +231 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +297 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +110 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
package/src/commands/router.ts
CHANGED
|
@@ -38,14 +38,38 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
|
|
|
38
38
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
|
-
import {
|
|
41
|
+
import { registerMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
42
|
+
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
43
|
+
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
44
|
+
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
45
|
+
import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
|
|
46
|
+
import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
|
|
47
|
+
import {
|
|
48
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
49
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
50
|
+
loadMeshRefineConfig,
|
|
51
|
+
resolveMeshRefineValidationPlan,
|
|
52
|
+
suggestMeshRefineConfig,
|
|
53
|
+
validateMeshRefineConfig,
|
|
54
|
+
type MeshRefineValidationCommandPlan,
|
|
55
|
+
} from '../mesh/refine-config.js';
|
|
56
|
+
import {
|
|
57
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
58
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
59
|
+
loadMeshWorktreeBootstrapConfig,
|
|
60
|
+
runMeshWorktreeBootstrap,
|
|
61
|
+
type WorktreeBootstrapState,
|
|
62
|
+
} from '../mesh/worktree-bootstrap-config.js';
|
|
42
63
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
43
64
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
44
65
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
66
|
+
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
45
67
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
46
|
-
import { homedir } from 'os';
|
|
47
|
-
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
68
|
+
import { homedir, hostname as osHostname } from 'os';
|
|
69
|
+
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
48
70
|
import * as fs from 'fs';
|
|
71
|
+
import { execFileSync } from 'node:child_process';
|
|
72
|
+
import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
|
|
49
73
|
|
|
50
74
|
type ReleaseChannel = 'stable' | 'preview';
|
|
51
75
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -114,14 +138,95 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
114
138
|
return undefined;
|
|
115
139
|
}
|
|
116
140
|
|
|
117
|
-
function
|
|
141
|
+
function summarizeRepoMeshDebugGit(git: unknown): Record<string, unknown> | null {
|
|
142
|
+
const record = readObjectRecord(git);
|
|
143
|
+
if (!Object.keys(record).length) return null;
|
|
144
|
+
const submodules = Array.isArray(record.submodules)
|
|
145
|
+
? record.submodules.map((entry: any) => ({
|
|
146
|
+
path: readStringValue(entry?.path) ?? null,
|
|
147
|
+
commit: readStringValue(entry?.commit)?.slice(0, 12) ?? null,
|
|
148
|
+
dirty: readBooleanValue(entry?.dirty) ?? false,
|
|
149
|
+
outOfSync: readBooleanValue(entry?.outOfSync, entry?.out_of_sync) ?? false,
|
|
150
|
+
}))
|
|
151
|
+
: [];
|
|
152
|
+
return {
|
|
153
|
+
isGitRepo: readBooleanValue(record.isGitRepo),
|
|
154
|
+
workspace: readStringValue(record.workspace) ?? null,
|
|
155
|
+
repoRoot: readStringValue(record.repoRoot, record.repo_root) ?? null,
|
|
156
|
+
branch: readStringValue(record.branch) ?? null,
|
|
157
|
+
upstream: readStringValue(record.upstream) ?? null,
|
|
158
|
+
upstreamStatus: readStringValue(record.upstreamStatus, record.upstream_status) ?? null,
|
|
159
|
+
headCommit: readStringValue(record.headCommit, record.head_commit)?.slice(0, 12) ?? null,
|
|
160
|
+
ahead: readNumberValue(record.ahead) ?? null,
|
|
161
|
+
behind: readNumberValue(record.behind) ?? null,
|
|
162
|
+
dirtyCounts: {
|
|
163
|
+
staged: readNumberValue(record.staged) ?? 0,
|
|
164
|
+
modified: readNumberValue(record.modified) ?? 0,
|
|
165
|
+
untracked: readNumberValue(record.untracked) ?? 0,
|
|
166
|
+
deleted: readNumberValue(record.deleted) ?? 0,
|
|
167
|
+
renamed: readNumberValue(record.renamed) ?? 0,
|
|
168
|
+
},
|
|
169
|
+
lastCheckedAt: readNumberValue(record.lastCheckedAt, record.last_checked_at) ?? null,
|
|
170
|
+
submoduleCount: submodules.length,
|
|
171
|
+
submodules,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
176
|
+
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
177
|
+
return {
|
|
178
|
+
success: status?.success,
|
|
179
|
+
meshId: readStringValue(status?.meshId, status?.mesh_id) ?? null,
|
|
180
|
+
refreshedAt: readStringValue(status?.refreshedAt, status?.refreshed_at) ?? null,
|
|
181
|
+
sourceOfTruth: status?.sourceOfTruth ?? null,
|
|
182
|
+
branchConvergenceSummary: status?.branchConvergenceSummary ?? status?.branch_convergence_summary ?? null,
|
|
183
|
+
nodeCount: nodes.length,
|
|
184
|
+
nodes: nodes.map((node: any) => ({
|
|
185
|
+
nodeId: readStringValue(node?.nodeId, node?.id) ?? null,
|
|
186
|
+
daemonId: readStringValue(node?.daemonId, node?.daemon_id) ?? null,
|
|
187
|
+
workspace: readStringValue(node?.workspace, node?.git?.workspace) ?? null,
|
|
188
|
+
health: readStringValue(node?.health) ?? null,
|
|
189
|
+
machineStatus: readStringValue(node?.machineStatus, node?.machine_status) ?? null,
|
|
190
|
+
connection: node?.connection && typeof node.connection === 'object' ? {
|
|
191
|
+
state: readStringValue(node.connection.state) ?? null,
|
|
192
|
+
transport: readStringValue(node.connection.transport) ?? null,
|
|
193
|
+
source: readStringValue(node.connection.source) ?? null,
|
|
194
|
+
reported: readBooleanValue(node.connection.reported) ?? null,
|
|
195
|
+
} : null,
|
|
196
|
+
gitProbePending: node?.gitProbePending === true,
|
|
197
|
+
launchReady: node?.launchReady === true,
|
|
198
|
+
git: summarizeRepoMeshDebugGit(node?.git),
|
|
199
|
+
branchConvergence: node?.branchConvergence ?? node?.branch_convergence ?? null,
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
205
|
+
try {
|
|
206
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
207
|
+
} catch {
|
|
208
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${event}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
|
|
213
|
+
const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : '';
|
|
214
|
+
const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : '';
|
|
215
|
+
if (!normalizedPath) return undefined;
|
|
216
|
+
if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath;
|
|
217
|
+
if (!normalizedRoot) return undefined;
|
|
218
|
+
return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
|
|
118
222
|
if (!Array.isArray(value)) return undefined;
|
|
119
223
|
const submodules = value
|
|
120
224
|
.map(entry => {
|
|
121
225
|
const submodule = readObjectRecord(entry);
|
|
122
226
|
const path = readStringValue(submodule.path);
|
|
123
227
|
const commit = readStringValue(submodule.commit);
|
|
124
|
-
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
228
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
229
|
+
?? joinRepoPath(parentRepoRoot, path);
|
|
125
230
|
if (!path || !commit || !repoPath) return null;
|
|
126
231
|
return {
|
|
127
232
|
path,
|
|
@@ -137,60 +242,146 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
|
137
242
|
return submodules.length > 0 ? submodules : undefined;
|
|
138
243
|
}
|
|
139
244
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
245
|
+
function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
|
|
246
|
+
const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
|
|
247
|
+
if (explicit) return explicit;
|
|
248
|
+
const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
|
|
249
|
+
const workspaceName = workspace ? pathBasename(workspace) : undefined;
|
|
250
|
+
const host = readStringValue(node.machineName, node.machine_name, node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
|
|
251
|
+
const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
|
|
252
|
+
const parts = [workspaceName, host, provider].filter(Boolean);
|
|
253
|
+
if (parts.length > 0) return parts.join(' · ');
|
|
254
|
+
return nodeId || 'unidentified mesh node';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeMeshHostname(value: unknown): string | undefined {
|
|
258
|
+
const hostname = readStringValue(value);
|
|
259
|
+
if (!hostname) return undefined;
|
|
260
|
+
return hostname.toLowerCase().replace(/\.$/, '');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
|
|
264
|
+
return readStringValue(
|
|
265
|
+
node.machineId,
|
|
266
|
+
node.machine_id,
|
|
267
|
+
readObjectRecord(node.machine)?.id,
|
|
268
|
+
readObjectRecord(node.machine)?.machineId,
|
|
269
|
+
readObjectRecord(node.lastProbe)?.machineId,
|
|
270
|
+
readObjectRecord(node.last_probe)?.machine_id,
|
|
271
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.id,
|
|
272
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.machineId,
|
|
273
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.id,
|
|
274
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.machine_id,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined {
|
|
279
|
+
return readStringValue(
|
|
280
|
+
node.daemonId,
|
|
281
|
+
node.daemon_id,
|
|
282
|
+
readObjectRecord(node.machine)?.daemonId,
|
|
283
|
+
readObjectRecord(node.machine)?.daemon_id,
|
|
284
|
+
readObjectRecord(node.lastProbe)?.daemonId,
|
|
285
|
+
readObjectRecord(node.last_probe)?.daemon_id,
|
|
286
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemonId,
|
|
287
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemon_id,
|
|
288
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemonId,
|
|
289
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemon_id,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
294
|
+
return readStringValue(
|
|
295
|
+
node.hostname,
|
|
296
|
+
node.host,
|
|
297
|
+
node.machineHostname,
|
|
298
|
+
node.machine_hostname,
|
|
299
|
+
readObjectRecord(node.machine)?.hostname,
|
|
300
|
+
readObjectRecord(node.machine)?.host,
|
|
301
|
+
readObjectRecord(node.lastProbe)?.hostname,
|
|
302
|
+
readObjectRecord(node.last_probe)?.hostname,
|
|
303
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.hostname,
|
|
304
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.hostname,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readMeshNodeDisplayMachineName(node: Record<string, unknown>): string | undefined {
|
|
309
|
+
return readStringValue(
|
|
310
|
+
node.machineName,
|
|
311
|
+
node.machine_name,
|
|
312
|
+
node.machineLabel,
|
|
313
|
+
node.machine_label,
|
|
314
|
+
node.machineNickname,
|
|
315
|
+
node.machine_nickname,
|
|
316
|
+
node.alias,
|
|
317
|
+
readObjectRecord(node.machine)?.name,
|
|
318
|
+
readObjectRecord(node.machine)?.displayName,
|
|
319
|
+
readObjectRecord(node.machine)?.display_name,
|
|
320
|
+
readObjectRecord(node.lastProbe)?.machineName,
|
|
321
|
+
readObjectRecord(node.last_probe)?.machine_name,
|
|
322
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.name,
|
|
323
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.name,
|
|
324
|
+
readMeshNodeHostname(node),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function compactMeshIdentityEvidence(value: string | undefined): string | undefined {
|
|
329
|
+
if (!value) return undefined;
|
|
330
|
+
return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
334
|
+
localMachineId?: string;
|
|
335
|
+
localDaemonId?: string;
|
|
336
|
+
coordinatorHostname?: string;
|
|
337
|
+
isSelfNode?: boolean;
|
|
338
|
+
}): Record<string, unknown> {
|
|
339
|
+
const machineId = readMeshNodeMachineId(node);
|
|
340
|
+
const daemonId = readMeshNodeDaemonId(node);
|
|
341
|
+
const hostname = readMeshNodeHostname(node);
|
|
342
|
+
const machineName = readMeshNodeDisplayMachineName(node);
|
|
343
|
+
const coordinatorHostname = readStringValue(opts.coordinatorHostname);
|
|
344
|
+
const machineIdMatches = Boolean(opts.localMachineId && machineId && opts.localMachineId === machineId);
|
|
345
|
+
const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && opts.localDaemonId === daemonId);
|
|
346
|
+
const hostnameMatches = Boolean(
|
|
347
|
+
normalizeMeshHostname(hostname)
|
|
348
|
+
&& normalizeMeshHostname(coordinatorHostname)
|
|
349
|
+
&& normalizeMeshHostname(hostname) === normalizeMeshHostname(coordinatorHostname),
|
|
350
|
+
);
|
|
351
|
+
const sameMachine = opts.isSelfNode === true || machineIdMatches || daemonIdMatches || hostnameMatches;
|
|
352
|
+
const evidence: string[] = [];
|
|
353
|
+
for (const [label, value] of [['machineName', machineName], ['hostname', hostname], ['machineId', machineId], ['daemonId', daemonId]] as const) {
|
|
354
|
+
const compact = compactMeshIdentityEvidence(value);
|
|
355
|
+
if (compact) evidence.push(`${label}:${compact}`);
|
|
174
356
|
}
|
|
357
|
+
const locality = sameMachine ? 'same_machine' : (evidence.length > 0 ? 'remote_known' : 'remote_or_unknown');
|
|
358
|
+
const localityReason = sameMachine
|
|
359
|
+
? (machineIdMatches ? 'matched coordinator machine id'
|
|
360
|
+
: daemonIdMatches ? 'matched coordinator daemon id'
|
|
361
|
+
: hostnameMatches ? 'matched coordinator hostname'
|
|
362
|
+
: 'selected coordinator node')
|
|
363
|
+
: evidence.length > 0
|
|
364
|
+
? `known remote/other machine identity; no local coordinator match (${evidence.join(', ')})`
|
|
365
|
+
: 'no useful machine identity evidence available';
|
|
366
|
+
return {
|
|
367
|
+
daemonId,
|
|
368
|
+
machineId,
|
|
369
|
+
hostname,
|
|
370
|
+
machineName,
|
|
371
|
+
displayName: machineName || hostname || daemonId || machineId,
|
|
372
|
+
coordinatorHostname,
|
|
373
|
+
sameMachine,
|
|
374
|
+
locality,
|
|
375
|
+
localityReason,
|
|
376
|
+
identityEvidence: evidence,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
175
379
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const probeGit = readObjectRecord(rawProbe.git);
|
|
182
|
-
const probeGitResult = readObjectRecord(probeGit.result);
|
|
183
|
-
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
184
|
-
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
185
|
-
const status = Object.keys(directStatus).length
|
|
186
|
-
? directStatus
|
|
187
|
-
: Object.keys(nestedStatus).length
|
|
188
|
-
? nestedStatus
|
|
189
|
-
: Object.keys(probeDirectStatus).length
|
|
190
|
-
? probeDirectStatus
|
|
191
|
-
: Object.keys(probeNestedStatus).length
|
|
192
|
-
? probeNestedStatus
|
|
193
|
-
: {};
|
|
380
|
+
function normalizeInlineMeshGitStatus(
|
|
381
|
+
status: Record<string, unknown>,
|
|
382
|
+
node: any,
|
|
383
|
+
options?: { lastCheckedAt?: number },
|
|
384
|
+
): Record<string, unknown> | undefined {
|
|
194
385
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
386
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
387
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -198,15 +389,20 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
198
389
|
: [];
|
|
199
390
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
391
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
-
const
|
|
392
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
393
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
202
394
|
return {
|
|
203
395
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
-
repoRoot:
|
|
396
|
+
repoRoot: repoRoot ?? null,
|
|
205
397
|
isGitRepo,
|
|
206
398
|
branch: readStringValue(status.branch) ?? null,
|
|
207
399
|
headCommit: readStringValue(status.headCommit) ?? null,
|
|
208
400
|
headMessage: readStringValue(status.headMessage) ?? null,
|
|
209
401
|
upstream: readStringValue(status.upstream) ?? null,
|
|
402
|
+
upstreamStatus: readStringValue(status.upstreamStatus, status.upstream_status)
|
|
403
|
+
?? (readStringValue(status.upstream) ? 'unchecked' : 'no_upstream'),
|
|
404
|
+
upstreamFetchedAt: readNumberValue(status.upstreamFetchedAt, status.upstream_fetched_at),
|
|
405
|
+
upstreamFetchError: readStringValue(status.upstreamFetchError, status.upstream_fetch_error),
|
|
210
406
|
ahead: readNumberValue(status.ahead) ?? 0,
|
|
211
407
|
behind: readNumberValue(status.behind) ?? 0,
|
|
212
408
|
staged: readNumberValue(status.staged) ?? 0,
|
|
@@ -217,14 +413,247 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
217
413
|
hasConflicts,
|
|
218
414
|
conflictFiles,
|
|
219
415
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
-
lastCheckedAt: Date.now(),
|
|
416
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
221
417
|
...(submodules ? { submodules } : {}),
|
|
222
418
|
};
|
|
223
419
|
}
|
|
224
420
|
|
|
421
|
+
function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
|
|
422
|
+
if (!git) return Number.NEGATIVE_INFINITY;
|
|
423
|
+
let score = 0;
|
|
424
|
+
if (readBooleanValue(git.isGitRepo) === true) score += 50;
|
|
425
|
+
if (readBooleanValue(git.isGitRepo) === false) score -= 10;
|
|
426
|
+
if (readStringValue(git.branch)) score += 20;
|
|
427
|
+
if (readStringValue(git.headCommit)) score += 20;
|
|
428
|
+
if (readStringValue(git.upstream)) score += 10;
|
|
429
|
+
if (readStringValue(git.upstreamStatus)) score += 5;
|
|
430
|
+
if (readNumberValue(git.ahead) !== undefined) score += 2;
|
|
431
|
+
if (readNumberValue(git.behind) !== undefined) score += 2;
|
|
432
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
|
|
433
|
+
if (readStringValue(git.error)) score -= 20;
|
|
434
|
+
return score;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
438
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
439
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
440
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
441
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
442
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
443
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
444
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
445
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
446
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
447
|
+
const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
|
|
448
|
+
let best: { git: Record<string, unknown>; score: number } | null = null;
|
|
449
|
+
for (const status of candidates) {
|
|
450
|
+
const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
451
|
+
if (!normalized) continue;
|
|
452
|
+
const score = scoreInlineMeshGitStatus(normalized);
|
|
453
|
+
if (!best || score > best.score) best = { git: normalized, score };
|
|
454
|
+
}
|
|
455
|
+
return best?.git;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
|
|
459
|
+
if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
|
|
460
|
+
return snapshot.nodes.some((node: any) => {
|
|
461
|
+
if (node?.gitProbePending !== true) return false;
|
|
462
|
+
const git = readObjectRecord(node?.git);
|
|
463
|
+
return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
468
|
+
const source = readStringValue(connection.source);
|
|
469
|
+
const transport = readStringValue(connection.transport);
|
|
470
|
+
return {
|
|
471
|
+
...connection,
|
|
472
|
+
perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
|
|
473
|
+
source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
|
|
474
|
+
state: 'connected',
|
|
475
|
+
transport: transport && transport !== 'unknown' ? transport : 'direct',
|
|
476
|
+
reported: true,
|
|
477
|
+
reason: 'Live peer git snapshot reported by the selected coordinator.',
|
|
478
|
+
lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function recordInlineMeshDirectGitTruth(
|
|
483
|
+
node: any,
|
|
484
|
+
git: Record<string, unknown>,
|
|
485
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
486
|
+
): void {
|
|
487
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
488
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
489
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
490
|
+
const nextGit: Record<string, unknown> = {
|
|
491
|
+
...git,
|
|
492
|
+
lastCheckedAt: checkedAt,
|
|
493
|
+
};
|
|
494
|
+
node.lastGit = {
|
|
495
|
+
source,
|
|
496
|
+
checkedAt,
|
|
497
|
+
status: nextGit,
|
|
498
|
+
};
|
|
499
|
+
node.last_git = node.lastGit;
|
|
500
|
+
node.machineStatus = 'online';
|
|
501
|
+
node.updatedAt = updatedAt;
|
|
502
|
+
node.lastSeenAt = updatedAt;
|
|
503
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
504
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
508
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
509
|
+
if (liveGit) return liveGit;
|
|
510
|
+
|
|
511
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
512
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
513
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
514
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
518
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
519
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
520
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
521
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
522
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
523
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
524
|
+
const branch = readStringValue(cachedGit.branch);
|
|
525
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
526
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
530
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
531
|
+
const {
|
|
532
|
+
cachedStatus,
|
|
533
|
+
lastGit: _lastGit,
|
|
534
|
+
last_git: _lastGitLegacy,
|
|
535
|
+
lastProbe: _lastProbe,
|
|
536
|
+
last_probe: _lastProbeLegacy,
|
|
537
|
+
error: _error,
|
|
538
|
+
health: _health,
|
|
539
|
+
machineStatus: _machineStatus,
|
|
540
|
+
lastSeenAt: _lastSeenAt,
|
|
541
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
542
|
+
updatedAt: _updatedAt,
|
|
543
|
+
updated_at: _updatedAtLegacy,
|
|
544
|
+
activeSession: _activeSession,
|
|
545
|
+
active_session: _activeSessionLegacy,
|
|
546
|
+
activeSessionId: _activeSessionId,
|
|
547
|
+
active_session_id: _activeSessionIdLegacy,
|
|
548
|
+
sessionId: _sessionId,
|
|
549
|
+
session_id: _sessionIdLegacy,
|
|
550
|
+
providerType: _providerType,
|
|
551
|
+
provider_type: _providerTypeLegacy,
|
|
552
|
+
...rest
|
|
553
|
+
} = node as Record<string, unknown>;
|
|
554
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
555
|
+
return { ...rest, cachedStatus };
|
|
556
|
+
}
|
|
557
|
+
return rest;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
561
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
562
|
+
return 'cachedStatus' in node
|
|
563
|
+
|| 'lastGit' in node
|
|
564
|
+
|| 'last_git' in node
|
|
565
|
+
|| 'lastProbe' in node
|
|
566
|
+
|| 'last_probe' in node
|
|
567
|
+
|| 'error' in node
|
|
568
|
+
|| 'health' in node
|
|
569
|
+
|| 'machineStatus' in node
|
|
570
|
+
|| 'lastSeenAt' in node
|
|
571
|
+
|| 'last_seen_at' in node
|
|
572
|
+
|| 'updatedAt' in node
|
|
573
|
+
|| 'updated_at' in node
|
|
574
|
+
|| 'activeSession' in node
|
|
575
|
+
|| 'active_session' in node
|
|
576
|
+
|| 'activeSessionId' in node
|
|
577
|
+
|| 'active_session_id' in node
|
|
578
|
+
|| 'sessionId' in node
|
|
579
|
+
|| 'session_id' in node
|
|
580
|
+
|| 'providerType' in node
|
|
581
|
+
|| 'provider_type' in node;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
585
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
586
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
587
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function readInlineMeshNodeId(node: any): string {
|
|
591
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
595
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
596
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
597
|
+
let changed = false;
|
|
598
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
599
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
600
|
+
changed = true;
|
|
601
|
+
return stripInlineMeshTransientNodeState(node);
|
|
602
|
+
});
|
|
603
|
+
if (!changed) return inlineMesh;
|
|
604
|
+
return {
|
|
605
|
+
...inlineMesh,
|
|
606
|
+
nodes,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
611
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
612
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
613
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
614
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
615
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
616
|
+
|
|
617
|
+
const cachedUpdatedAt = Date.parse(readStringValue(cached.updatedAt, cached.updated_at) || '');
|
|
618
|
+
const incomingUpdatedAt = Date.parse(readStringValue(incoming.updatedAt, incoming.updated_at) || '');
|
|
619
|
+
const preserveCachedMembership = Number.isFinite(cachedUpdatedAt)
|
|
620
|
+
&& (!Number.isFinite(incomingUpdatedAt) || cachedUpdatedAt > incomingUpdatedAt);
|
|
621
|
+
|
|
622
|
+
const cachedById = new Map<string, any>();
|
|
623
|
+
for (const node of cachedNodes) {
|
|
624
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
625
|
+
if (nodeId) cachedById.set(nodeId, node);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const nodes = incomingNodes.map((incomingNode: any) => {
|
|
629
|
+
const nodeId = readInlineMeshNodeId(incomingNode);
|
|
630
|
+
const cachedNode = nodeId ? cachedById.get(nodeId) : undefined;
|
|
631
|
+
if (!cachedNode && preserveCachedMembership) return null;
|
|
632
|
+
if (!cachedNode) return incomingNode;
|
|
633
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
634
|
+
return { ...cachedNode, ...incomingNode };
|
|
635
|
+
}
|
|
636
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
637
|
+
}).filter(Boolean);
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
...cached,
|
|
641
|
+
...incoming,
|
|
642
|
+
nodes,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
225
646
|
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
226
|
-
|
|
227
|
-
|
|
647
|
+
return countGitWorktreeChanges(git) > 0;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function countGitWorktreeChanges(git: Record<string, unknown> | null | undefined): number {
|
|
651
|
+
if (!git) return 0;
|
|
652
|
+
return Number(git.staged || 0)
|
|
653
|
+
+ Number(git.modified || 0)
|
|
654
|
+
+ Number(git.untracked || 0)
|
|
655
|
+
+ Number(git.deleted || 0)
|
|
656
|
+
+ Number(git.renamed || 0);
|
|
228
657
|
}
|
|
229
658
|
|
|
230
659
|
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
@@ -249,6 +678,167 @@ function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undef
|
|
|
249
678
|
return 'online';
|
|
250
679
|
}
|
|
251
680
|
|
|
681
|
+
function readMeshNodeLabel(status: Record<string, unknown>, node: any): string {
|
|
682
|
+
return readStringValue(status.nodeId, node?.id, node?.nodeId) ?? 'unknown';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function buildInlineMeshBranchConvergence(args: {
|
|
686
|
+
mesh: any;
|
|
687
|
+
node: any;
|
|
688
|
+
status: Record<string, unknown>;
|
|
689
|
+
}): Record<string, unknown> {
|
|
690
|
+
const git = readObjectRecord(args.status.git);
|
|
691
|
+
const nodeLabel = readMeshNodeLabel(args.status, args.node);
|
|
692
|
+
const defaultBranch = readStringValue(args.mesh?.defaultBranch) ?? 'main';
|
|
693
|
+
const branch = readStringValue(git.branch, args.node?.worktreeBranch) ?? null;
|
|
694
|
+
const upstream = readStringValue(git.upstream) ?? null;
|
|
695
|
+
const upstreamStatus = readStringValue(git.upstreamStatus, git.upstream_status)
|
|
696
|
+
?? (upstream ? 'unchecked' : 'no_upstream');
|
|
697
|
+
const ahead = readNumberValue(git.ahead) ?? 0;
|
|
698
|
+
const behind = readNumberValue(git.behind) ?? 0;
|
|
699
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
700
|
+
const hasConflicts = readBooleanValue(git.hasConflicts)
|
|
701
|
+
?? (Array.isArray(git.conflictFiles) && git.conflictFiles.length > 0);
|
|
702
|
+
const base = {
|
|
703
|
+
defaultBranch,
|
|
704
|
+
branch,
|
|
705
|
+
upstream,
|
|
706
|
+
upstreamStatus,
|
|
707
|
+
ahead,
|
|
708
|
+
behind,
|
|
709
|
+
isWorktree: args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true,
|
|
710
|
+
isDefaultBranch: branch === defaultBranch,
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
if (readBooleanValue(git.isGitRepo) !== true) {
|
|
714
|
+
return {
|
|
715
|
+
...base,
|
|
716
|
+
status: 'blocked_review',
|
|
717
|
+
needsConvergence: true,
|
|
718
|
+
reason: 'git_status_unavailable',
|
|
719
|
+
nextStep: `Resolve git status for node '${nodeLabel}' before marking the task complete.`,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (!branch) {
|
|
724
|
+
return {
|
|
725
|
+
...base,
|
|
726
|
+
status: 'blocked_review',
|
|
727
|
+
needsConvergence: true,
|
|
728
|
+
reason: 'branch_unknown',
|
|
729
|
+
nextStep: `Inspect node '${nodeLabel}' git branch before deciding whether it is merged to ${defaultBranch}.`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (hasConflicts || uncommittedChanges > 0) {
|
|
734
|
+
return {
|
|
735
|
+
...base,
|
|
736
|
+
status: 'not_mergeable',
|
|
737
|
+
needsConvergence: true,
|
|
738
|
+
reason: hasConflicts ? 'conflicts_present' : 'dirty_workspace',
|
|
739
|
+
nextStep: `Commit, checkpoint, or resolve node '${nodeLabel}' before any main convergence step.`,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (branch === defaultBranch) {
|
|
744
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
745
|
+
return {
|
|
746
|
+
...base,
|
|
747
|
+
status: 'blocked_review',
|
|
748
|
+
needsConvergence: true,
|
|
749
|
+
reason: 'default_branch_upstream_unverified',
|
|
750
|
+
nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${nodeLabel}'.`,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
if (ahead > 0 || behind > 0) {
|
|
754
|
+
return {
|
|
755
|
+
...base,
|
|
756
|
+
status: 'blocked_review',
|
|
757
|
+
needsConvergence: true,
|
|
758
|
+
reason: 'default_branch_not_even_with_upstream',
|
|
759
|
+
nextStep: `Bring ${defaultBranch} even with its upstream before declaring convergence complete.`,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
...base,
|
|
764
|
+
status: 'merged_to_main',
|
|
765
|
+
needsConvergence: false,
|
|
766
|
+
reason: 'clean_default_branch',
|
|
767
|
+
nextStep: null,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true) {
|
|
772
|
+
return {
|
|
773
|
+
...base,
|
|
774
|
+
status: 'cleanup_candidate',
|
|
775
|
+
needsConvergence: true,
|
|
776
|
+
reason: 'clean_non_default_worktree_branch',
|
|
777
|
+
nextStep: `Run mesh_refine_node(node_id: "${nodeLabel}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
782
|
+
return {
|
|
783
|
+
...base,
|
|
784
|
+
status: 'blocked_review',
|
|
785
|
+
needsConvergence: true,
|
|
786
|
+
reason: 'feature_branch_upstream_unverified',
|
|
787
|
+
nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (!upstream || ahead > 0 || behind > 0) {
|
|
792
|
+
return {
|
|
793
|
+
...base,
|
|
794
|
+
status: 'blocked_review',
|
|
795
|
+
needsConvergence: true,
|
|
796
|
+
reason: !upstream ? 'feature_branch_missing_upstream' : 'feature_branch_not_even_with_upstream',
|
|
797
|
+
nextStep: `Push or reconcile branch '${branch}', then merge it into ${defaultBranch} or mark it not_mergeable with a reason.`,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
...base,
|
|
803
|
+
status: 'pushed_feature_branch_needs_merge',
|
|
804
|
+
needsConvergence: true,
|
|
805
|
+
reason: 'clean_non_default_branch',
|
|
806
|
+
nextStep: `Review and merge branch '${branch}' into ${defaultBranch}; do not report the task as fully complete while it remains off main.`,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
811
|
+
const git = readObjectRecord(status.git);
|
|
812
|
+
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
813
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
814
|
+
status.isDirty = uncommittedChanges > 0;
|
|
815
|
+
status.uncommittedChanges = uncommittedChanges;
|
|
816
|
+
status.branchConvergence = buildInlineMeshBranchConvergence({ mesh, node, status });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
820
|
+
const followUps = nodes
|
|
821
|
+
.filter(node => readObjectRecord(node.branchConvergence).needsConvergence === true)
|
|
822
|
+
.map(node => {
|
|
823
|
+
const convergence = readObjectRecord(node.branchConvergence);
|
|
824
|
+
return {
|
|
825
|
+
nodeId: node.nodeId,
|
|
826
|
+
workspace: node.workspace,
|
|
827
|
+
branch: convergence.branch,
|
|
828
|
+
status: convergence.status,
|
|
829
|
+
reason: convergence.reason,
|
|
830
|
+
nextStep: convergence.nextStep,
|
|
831
|
+
};
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
needsFollowUp: followUps.length > 0,
|
|
836
|
+
unresolvedCount: followUps.length,
|
|
837
|
+
requiredFinalStates: ['merged_to_main', 'pushed_feature_branch_needs_merge', 'blocked_review', 'cleanup_candidate', 'not_mergeable'],
|
|
838
|
+
followUps,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
252
842
|
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
253
843
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
254
844
|
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
@@ -313,6 +903,169 @@ function toIsoTimestamp(value: unknown): string | null {
|
|
|
313
903
|
return stringValue || null;
|
|
314
904
|
}
|
|
315
905
|
|
|
906
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
907
|
+
const connection = readObjectRecord(status.connection);
|
|
908
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
909
|
+
const git = readObjectRecord(status.git);
|
|
910
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
911
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
912
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
913
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function finalizeMeshNodeStatus(args: {
|
|
918
|
+
status: Record<string, unknown>;
|
|
919
|
+
node: any;
|
|
920
|
+
daemonId?: string;
|
|
921
|
+
isSelfNode: boolean;
|
|
922
|
+
}): void {
|
|
923
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
924
|
+
if (!readStringValue(status.machineStatus)) {
|
|
925
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
926
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
927
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
928
|
+
}
|
|
929
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
930
|
+
const bootstrap = readObjectRecord(node?.worktreeBootstrap);
|
|
931
|
+
if (node?.isLocalWorktree && readStringValue(bootstrap.status)) {
|
|
932
|
+
status.worktreeBootstrap = bootstrap;
|
|
933
|
+
if (bootstrap.status === 'failed' && bootstrap.required !== false) {
|
|
934
|
+
status.launchReady = false;
|
|
935
|
+
status.launchBlockedReason = 'worktree_bootstrap_failed';
|
|
936
|
+
status.launchBlockedMessage = readStringValue(bootstrap.error)
|
|
937
|
+
|| 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (bootstrap.status === 'running' && bootstrap.required !== false) {
|
|
941
|
+
status.launchReady = false;
|
|
942
|
+
status.launchBlockedReason = 'worktree_bootstrap_running';
|
|
943
|
+
status.launchBlockedMessage = 'Required worktree bootstrap is still running; wait for it to finish before launching an agent into this node.';
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
948
|
+
status.launchReady = !!daemonId && (
|
|
949
|
+
readStringValue(status.machineStatus) === 'online'
|
|
950
|
+
|| connectionState === 'connected'
|
|
951
|
+
|| isSelfNode
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
956
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
957
|
+
daemonId: string;
|
|
958
|
+
workspace: string;
|
|
959
|
+
timeoutMs: number;
|
|
960
|
+
}): Promise<Record<string, unknown> | null> {
|
|
961
|
+
if (!args.dispatchMeshCommand) return null;
|
|
962
|
+
const remoteResult = await Promise.race([
|
|
963
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
|
|
964
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
965
|
+
]) as any;
|
|
966
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
967
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
968
|
+
? remoteGit as Record<string, unknown>
|
|
969
|
+
: null;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
973
|
+
mesh: any;
|
|
974
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
975
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
976
|
+
statusInstanceId?: string;
|
|
977
|
+
localMachineId?: string;
|
|
978
|
+
}): Promise<{
|
|
979
|
+
directEvidenceCount: number;
|
|
980
|
+
localConfirmedCount: number;
|
|
981
|
+
peerAttemptedCount: number;
|
|
982
|
+
peerConfirmedCount: number;
|
|
983
|
+
unavailableNodeIds: string[];
|
|
984
|
+
}> {
|
|
985
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
986
|
+
if (!nodes.length) {
|
|
987
|
+
return {
|
|
988
|
+
directEvidenceCount: 0,
|
|
989
|
+
localConfirmedCount: 0,
|
|
990
|
+
peerAttemptedCount: 0,
|
|
991
|
+
peerConfirmedCount: 0,
|
|
992
|
+
unavailableNodeIds: [],
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
997
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
998
|
+
nodes[0]?.id,
|
|
999
|
+
nodes[0]?.nodeId,
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
let localConfirmedCount = 0;
|
|
1003
|
+
let peerAttemptedCount = 0;
|
|
1004
|
+
let peerConfirmedCount = 0;
|
|
1005
|
+
const unavailableNodeIds: string[] = [];
|
|
1006
|
+
|
|
1007
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
1008
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
1009
|
+
const workspace = readStringValue(node?.workspace);
|
|
1010
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
1011
|
+
const isSelfNode = Boolean(
|
|
1012
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
1013
|
+
) || Boolean(
|
|
1014
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
1015
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
1016
|
+
|
|
1017
|
+
if (!workspace) {
|
|
1018
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (fs.existsSync(workspace)) {
|
|
1023
|
+
try {
|
|
1024
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
1025
|
+
if (localGit?.isGitRepo) {
|
|
1026
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
1027
|
+
localConfirmedCount += 1;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
} catch {
|
|
1031
|
+
// Fall through to remote classification.
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
1036
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
peerAttemptedCount += 1;
|
|
1041
|
+
try {
|
|
1042
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
1043
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1044
|
+
daemonId,
|
|
1045
|
+
workspace,
|
|
1046
|
+
timeoutMs: 8_000,
|
|
1047
|
+
});
|
|
1048
|
+
if (remoteGit) {
|
|
1049
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1050
|
+
peerConfirmedCount += 1;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
} catch {
|
|
1054
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
unavailableNodeIds.push(nodeId);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return {
|
|
1061
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
1062
|
+
localConfirmedCount,
|
|
1063
|
+
peerAttemptedCount,
|
|
1064
|
+
peerConfirmedCount,
|
|
1065
|
+
unavailableNodeIds,
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
316
1069
|
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
317
1070
|
return {
|
|
318
1071
|
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
@@ -328,11 +1081,140 @@ function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
|
328
1081
|
};
|
|
329
1082
|
}
|
|
330
1083
|
|
|
331
|
-
function
|
|
1084
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string, nodeWorkspace = '', nodeIsMissingLocalWorktree = false): boolean {
|
|
1085
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1086
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
1087
|
+
if (nodeIsMissingLocalWorktree) return false;
|
|
1088
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
1089
|
+
if (nodeWorkspace && recordWorkspace && recordWorkspace !== nodeWorkspace) return false;
|
|
1090
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1091
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
1095
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
1096
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
1097
|
+
|
|
1098
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1099
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
1100
|
+
|
|
1101
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function readLiveMeshNodeWorkspace(args: {
|
|
1105
|
+
meshId: string;
|
|
1106
|
+
nodeId: string;
|
|
1107
|
+
liveSessionRecords: any[];
|
|
1108
|
+
allowCoordinatorSession?: boolean;
|
|
1109
|
+
}): string {
|
|
1110
|
+
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
1111
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
1112
|
+
&& readStringValue(record?.workspace)
|
|
1113
|
+
));
|
|
1114
|
+
if (directNodeWorkspace) {
|
|
1115
|
+
return readStringValue(directNodeWorkspace.workspace) || '';
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (args.allowCoordinatorSession) {
|
|
1119
|
+
const coordinatorWorkspace = args.liveSessionRecords.find((record) => (
|
|
1120
|
+
readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId
|
|
1121
|
+
&& readStringValue(record?.workspace)
|
|
1122
|
+
));
|
|
1123
|
+
if (coordinatorWorkspace) {
|
|
1124
|
+
return readStringValue(coordinatorWorkspace.workspace) || '';
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return '';
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function collectLiveMeshSessionRecords(args: {
|
|
1132
|
+
meshId: string;
|
|
1133
|
+
node: any;
|
|
1134
|
+
nodeId: string;
|
|
1135
|
+
liveSessionRecords: any[];
|
|
1136
|
+
allowCoordinatorSession?: boolean;
|
|
1137
|
+
}): any[] {
|
|
1138
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
1139
|
+
const nodeIsMissingLocalWorktree = args.node?.isLocalWorktree === true
|
|
1140
|
+
&& !!nodeWorkspace
|
|
1141
|
+
&& !fs.existsSync(nodeWorkspace);
|
|
1142
|
+
const matches = args.liveSessionRecords.filter((record) => {
|
|
1143
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1144
|
+
if (recordNodeId && recordNodeId !== args.nodeId) return false;
|
|
1145
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId, nodeWorkspace || '', nodeIsMissingLocalWorktree)) return true;
|
|
1146
|
+
if (nodeIsMissingLocalWorktree) return false;
|
|
1147
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
if (args.allowCoordinatorSession) {
|
|
1151
|
+
for (const record of args.liveSessionRecords) {
|
|
1152
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) !== args.meshId) continue;
|
|
1153
|
+
const sessionId = readStringValue(record?.sessionId);
|
|
1154
|
+
if (sessionId && matches.some((entry) => readStringValue(entry?.sessionId) === sessionId)) continue;
|
|
1155
|
+
matches.push(record);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return matches;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function buildHistoricalMeshSessions(args: {
|
|
1163
|
+
meshId: string;
|
|
1164
|
+
nodes: any[];
|
|
1165
|
+
liveSessionRecords: any[];
|
|
1166
|
+
}): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
|
|
1167
|
+
const liveNodeIds = new Set<string>();
|
|
1168
|
+
const liveWorkspaces = new Set<string>();
|
|
1169
|
+
const missingLocalWorktreeNodeIds = new Set<string>();
|
|
1170
|
+
for (const node of args.nodes || []) {
|
|
1171
|
+
const nodeId = readStringValue(node?.id, node?.nodeId);
|
|
1172
|
+
const workspace = readStringValue(node?.workspace);
|
|
1173
|
+
if (nodeId) liveNodeIds.add(nodeId);
|
|
1174
|
+
if (workspace) liveWorkspaces.add(workspace);
|
|
1175
|
+
if (nodeId && node?.isLocalWorktree === true && workspace && !fs.existsSync(workspace)) {
|
|
1176
|
+
missingLocalWorktreeNodeIds.add(nodeId);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const sessions: Record<string, unknown>[] = [];
|
|
1181
|
+
for (const record of args.liveSessionRecords || []) {
|
|
1182
|
+
const meta = readObjectRecord(record?.meta);
|
|
1183
|
+
const recordMeshId = readStringValue(meta.meshNodeFor, meta.meshCoordinatorFor);
|
|
1184
|
+
if (recordMeshId !== args.meshId) continue;
|
|
1185
|
+
const recordNodeId = readStringValue(meta.meshNodeId);
|
|
1186
|
+
const workspace = readStringValue(record?.workspace);
|
|
1187
|
+
const removedNode = !!recordNodeId && (!liveNodeIds.has(recordNodeId) || missingLocalWorktreeNodeIds.has(recordNodeId));
|
|
1188
|
+
const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
|
|
1189
|
+
if (!removedNode && !orphanedWorkspace) continue;
|
|
1190
|
+
sessions.push({
|
|
1191
|
+
...summarizeMeshSessionRecord(record),
|
|
1192
|
+
classification: removedNode ? 'removedNode' : 'orphanedSession',
|
|
1193
|
+
historical: true,
|
|
1194
|
+
meshNodeId: recordNodeId || null,
|
|
1195
|
+
reason: removedNode
|
|
1196
|
+
? 'Session is tagged to a mesh node that is no longer in live membership.'
|
|
1197
|
+
: 'Session workspace is no longer attached to a live mesh node.',
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
if (sessions.length === 0) return undefined;
|
|
1201
|
+
return {
|
|
1202
|
+
count: sessions.length,
|
|
1203
|
+
sessions: sessions.slice(0, 5),
|
|
1204
|
+
instruction: 'These sessions are separated from normal node activeSessions because their mesh node/workspace is no longer live. Use mesh_cleanup_sessions only if cleanup is intended.',
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function applyCachedInlineMeshNodeStatus(
|
|
1209
|
+
status: Record<string, unknown>,
|
|
1210
|
+
node: any,
|
|
1211
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
1212
|
+
): boolean {
|
|
332
1213
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const
|
|
1214
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
1215
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
1216
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
1217
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
336
1218
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
337
1219
|
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
338
1220
|
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
@@ -389,29 +1271,140 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
389
1271
|
}
|
|
390
1272
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
391
1273
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
392
|
-
type MeshRefineValidationCommand =
|
|
393
|
-
command: string;
|
|
394
|
-
args: string[];
|
|
395
|
-
displayCommand: string;
|
|
396
|
-
category: string;
|
|
397
|
-
source: string;
|
|
398
|
-
};
|
|
1274
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
399
1275
|
|
|
400
1276
|
type MeshRefineValidationSummary = {
|
|
401
1277
|
status: MeshRefineValidationStatus;
|
|
402
1278
|
required: true;
|
|
403
1279
|
commandsRun: Array<Record<string, unknown>>;
|
|
1280
|
+
bootstrapCommandsRun: Array<Record<string, unknown>>;
|
|
404
1281
|
rejectedCommands: Array<Record<string, unknown>>;
|
|
405
1282
|
skippedReason?: string;
|
|
1283
|
+
failureKind?: string;
|
|
1284
|
+
failureCode?: string;
|
|
406
1285
|
timeoutMs: number;
|
|
407
1286
|
outputLimitBytes: number;
|
|
1287
|
+
configSource?: string;
|
|
1288
|
+
configSourceType?: string;
|
|
1289
|
+
suggestions?: unknown[];
|
|
1290
|
+
suggestedConfig?: unknown;
|
|
408
1291
|
};
|
|
409
1292
|
|
|
1293
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
1294
|
+
|
|
1295
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
1296
|
+
status: MeshRefineStageStatus;
|
|
1297
|
+
equivalent: boolean;
|
|
1298
|
+
baseHead: string;
|
|
1299
|
+
branchHead: string;
|
|
1300
|
+
mergeBase?: string;
|
|
1301
|
+
mergedTree?: string;
|
|
1302
|
+
expectedPatchId?: string;
|
|
1303
|
+
actualPatchId?: string;
|
|
1304
|
+
durationMs: number;
|
|
1305
|
+
error?: string;
|
|
1306
|
+
stdout?: string;
|
|
1307
|
+
stderr?: string;
|
|
1308
|
+
actionableHint?: MeshRefineSubmoduleConflictHint;
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
type MeshRefineSubmoduleConflictHint = {
|
|
1312
|
+
kind: 'submodule_conflict';
|
|
1313
|
+
message: string;
|
|
1314
|
+
conflicts: Array<{
|
|
1315
|
+
path: string;
|
|
1316
|
+
baseCommit?: string;
|
|
1317
|
+
branchCommit?: string;
|
|
1318
|
+
}>;
|
|
1319
|
+
nextSteps: string[];
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
type MeshRefineSubmoduleAlignmentSummary = {
|
|
1323
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
1324
|
+
changedGitlinkPaths: string[];
|
|
1325
|
+
outOfSyncPaths: string[];
|
|
1326
|
+
updatedPaths: string[];
|
|
1327
|
+
verifiedPaths: string[];
|
|
1328
|
+
durationMs: number;
|
|
1329
|
+
reason?: string;
|
|
1330
|
+
command?: string;
|
|
1331
|
+
error?: string;
|
|
1332
|
+
stdout?: string;
|
|
1333
|
+
stderr?: string;
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
type MeshRefineSubmoduleReachabilityEntry = {
|
|
1337
|
+
path: string;
|
|
1338
|
+
commit: string;
|
|
1339
|
+
reachable: boolean;
|
|
1340
|
+
publishRequired?: boolean;
|
|
1341
|
+
autoPublishAllowed?: boolean;
|
|
1342
|
+
autoPublishAttempted?: boolean;
|
|
1343
|
+
autoPublishSucceeded?: boolean;
|
|
1344
|
+
autoPublishVerified?: boolean;
|
|
1345
|
+
autoPublishRefspec?: string;
|
|
1346
|
+
autoPublishSkippedReason?: string;
|
|
1347
|
+
importedFromWorktree?: boolean;
|
|
1348
|
+
checkedLocal?: boolean;
|
|
1349
|
+
localReachable?: boolean;
|
|
1350
|
+
remote?: string;
|
|
1351
|
+
remoteUrl?: string;
|
|
1352
|
+
remoteReachable?: boolean;
|
|
1353
|
+
remoteMainBranch?: string;
|
|
1354
|
+
remoteMainReachable?: boolean;
|
|
1355
|
+
fetchedFromOrigin?: boolean;
|
|
1356
|
+
error?: string;
|
|
1357
|
+
publishStdout?: string;
|
|
1358
|
+
publishStderr?: string;
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
type MeshRefineSubmoduleReachabilitySummary = {
|
|
1362
|
+
status: MeshRefineStageStatus;
|
|
1363
|
+
checked: number;
|
|
1364
|
+
unreachable: MeshRefineSubmoduleReachabilityEntry[];
|
|
1365
|
+
entries: MeshRefineSubmoduleReachabilityEntry[];
|
|
1366
|
+
durationMs: number;
|
|
1367
|
+
autoPublishAllowed?: boolean;
|
|
1368
|
+
autoPublishPolicySource?: string;
|
|
1369
|
+
error?: string;
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1373
|
+
|
|
1374
|
+
type MeshRefineJobHandle = {
|
|
1375
|
+
success: true;
|
|
1376
|
+
async: true;
|
|
1377
|
+
status: MeshRefineAsyncJobStatus;
|
|
1378
|
+
jobId: string;
|
|
1379
|
+
interactionId: string;
|
|
1380
|
+
meshId: string;
|
|
1381
|
+
nodeId: string;
|
|
1382
|
+
targetNodeId: string;
|
|
1383
|
+
targetDaemonId?: string;
|
|
1384
|
+
workspace?: string;
|
|
1385
|
+
startedAt: string;
|
|
1386
|
+
completedAt?: string;
|
|
1387
|
+
duplicate?: boolean;
|
|
1388
|
+
retryOfJobId?: string;
|
|
1389
|
+
eventDelivery: {
|
|
1390
|
+
pendingEvents: true;
|
|
1391
|
+
ledger: true;
|
|
1392
|
+
};
|
|
1393
|
+
evidence: {
|
|
1394
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
1395
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
1396
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
1397
|
+
};
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
1401
|
+
|
|
410
1402
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
411
1403
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
412
1404
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
413
1405
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
414
1406
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
1407
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
415
1408
|
|
|
416
1409
|
function truncateValidationOutput(value: unknown): string {
|
|
417
1410
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -419,171 +1412,492 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
419
1412
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
420
1413
|
}
|
|
421
1414
|
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const trimmed = command.trim();
|
|
436
|
-
if (!trimmed) return null;
|
|
437
|
-
// Fail closed: the gate never hands shell syntax to a shell. Package-manager
|
|
438
|
-
// scripts are invoked via execFile(binary, args), and metacharacters/quotes are
|
|
439
|
-
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
440
|
-
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
441
|
-
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
442
|
-
if (!tokens.length) return null;
|
|
443
|
-
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
444
|
-
return tokens;
|
|
1415
|
+
function recordMeshRefineStage(
|
|
1416
|
+
stages: Array<Record<string, unknown>>,
|
|
1417
|
+
stage: string,
|
|
1418
|
+
status: MeshRefineStageStatus,
|
|
1419
|
+
startedAt: number,
|
|
1420
|
+
details?: Record<string, unknown>,
|
|
1421
|
+
): void {
|
|
1422
|
+
stages.push({
|
|
1423
|
+
stage,
|
|
1424
|
+
status,
|
|
1425
|
+
durationMs: Date.now() - startedAt,
|
|
1426
|
+
...(details || {}),
|
|
1427
|
+
});
|
|
445
1428
|
}
|
|
446
1429
|
|
|
447
|
-
function
|
|
448
|
-
|
|
1430
|
+
function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
|
|
1431
|
+
const refs = entries
|
|
1432
|
+
.map(entry => `${entry.path}@${entry.commit}`)
|
|
1433
|
+
.join(', ');
|
|
1434
|
+
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
|
|
449
1435
|
}
|
|
450
1436
|
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
)
|
|
457
|
-
|
|
458
|
-
if (!tokens) {
|
|
459
|
-
return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
|
|
1437
|
+
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1438
|
+
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1439
|
+
process.stderr.write(
|
|
1440
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via mesh.policy. `
|
|
1441
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
1442
|
+
);
|
|
1443
|
+
return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
|
|
460
1444
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
scriptName = third;
|
|
469
|
-
args = ['run', scriptName, ...rest];
|
|
470
|
-
} else if (binary === 'npm' && second === 'test' && !third) {
|
|
471
|
-
scriptName = 'test';
|
|
472
|
-
args = ['test'];
|
|
473
|
-
} else if (binary === 'yarn' && second === 'run' && third) {
|
|
474
|
-
scriptName = third;
|
|
475
|
-
args = ['run', scriptName, ...rest];
|
|
476
|
-
} else if (binary === 'yarn' && second && !third) {
|
|
477
|
-
scriptName = second;
|
|
478
|
-
args = [scriptName];
|
|
479
|
-
} else {
|
|
480
|
-
return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
|
|
1445
|
+
const loaded = loadMeshRefineConfig(mesh, workspace);
|
|
1446
|
+
if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1447
|
+
process.stderr.write(
|
|
1448
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via ${loaded.path || loaded.source}. `
|
|
1449
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
1450
|
+
);
|
|
1451
|
+
return { enabled: true, source: loaded.path || loaded.source };
|
|
481
1452
|
}
|
|
1453
|
+
return { enabled: false };
|
|
1454
|
+
}
|
|
482
1455
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1456
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
1457
|
+
const { execFileSync } = await import('node:child_process');
|
|
1458
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
1459
|
+
cwd,
|
|
1460
|
+
encoding: 'utf8',
|
|
1461
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1462
|
+
});
|
|
1463
|
+
if (!diff.trim()) return '';
|
|
1464
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
1465
|
+
cwd,
|
|
1466
|
+
input: diff,
|
|
1467
|
+
encoding: 'utf8',
|
|
1468
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1469
|
+
}).trim();
|
|
1470
|
+
return patchId.split(/\s+/)[0] || '';
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
1474
|
+
repoRoot: string,
|
|
1475
|
+
baseHead: string,
|
|
1476
|
+
branchHead: string,
|
|
1477
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
1478
|
+
const startedAt = Date.now();
|
|
1479
|
+
try {
|
|
1480
|
+
const { execFileSync } = await import('node:child_process');
|
|
1481
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
1482
|
+
cwd: repoRoot,
|
|
1483
|
+
encoding: 'utf8',
|
|
1484
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1485
|
+
});
|
|
1486
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
1487
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
1488
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
1489
|
+
if (!mergeBase || !mergedTree) {
|
|
1490
|
+
return {
|
|
1491
|
+
status: 'failed',
|
|
1492
|
+
equivalent: false,
|
|
1493
|
+
baseHead,
|
|
1494
|
+
branchHead,
|
|
1495
|
+
mergeBase: mergeBase || undefined,
|
|
1496
|
+
mergedTree: mergedTree || undefined,
|
|
1497
|
+
durationMs: Date.now() - startedAt,
|
|
1498
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
1499
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
1503
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
1504
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
1505
|
+
return {
|
|
1506
|
+
status: equivalent ? 'passed' : 'failed',
|
|
1507
|
+
equivalent,
|
|
1508
|
+
baseHead,
|
|
1509
|
+
branchHead,
|
|
1510
|
+
mergeBase,
|
|
1511
|
+
mergedTree,
|
|
1512
|
+
expectedPatchId,
|
|
1513
|
+
actualPatchId,
|
|
1514
|
+
durationMs: Date.now() - startedAt,
|
|
1515
|
+
};
|
|
1516
|
+
} catch (e: any) {
|
|
1517
|
+
return {
|
|
1518
|
+
status: 'failed',
|
|
1519
|
+
equivalent: false,
|
|
1520
|
+
baseHead,
|
|
1521
|
+
branchHead,
|
|
1522
|
+
durationMs: Date.now() - startedAt,
|
|
1523
|
+
error: e?.message || String(e),
|
|
1524
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1525
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1526
|
+
actionableHint: buildPatchEquivalenceSubmoduleConflictHint(
|
|
1527
|
+
repoRoot,
|
|
1528
|
+
baseHead,
|
|
1529
|
+
branchHead,
|
|
1530
|
+
`${e?.message || ''}\n${e?.stdout || ''}\n${e?.stderr || ''}`,
|
|
1531
|
+
),
|
|
1532
|
+
};
|
|
488
1533
|
}
|
|
1534
|
+
}
|
|
489
1535
|
|
|
1536
|
+
function buildPatchEquivalenceSubmoduleConflictHint(
|
|
1537
|
+
repoRoot: string,
|
|
1538
|
+
baseHead: string,
|
|
1539
|
+
branchHead: string,
|
|
1540
|
+
output: string,
|
|
1541
|
+
): MeshRefineSubmoduleConflictHint | undefined {
|
|
1542
|
+
if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return undefined;
|
|
1543
|
+
const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead)
|
|
1544
|
+
.map(path => ({
|
|
1545
|
+
path,
|
|
1546
|
+
baseCommit: readTreeObject(repoRoot, baseHead, path),
|
|
1547
|
+
branchCommit: readTreeObject(repoRoot, branchHead, path),
|
|
1548
|
+
}));
|
|
1549
|
+
if (conflicts.length === 0) return undefined;
|
|
490
1550
|
return {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1551
|
+
kind: 'submodule_conflict',
|
|
1552
|
+
message: 'Refinery could not synthesize a safe merge tree because the branch and base point the same submodule path at different commits.',
|
|
1553
|
+
conflicts,
|
|
1554
|
+
nextSteps: [
|
|
1555
|
+
'Inspect the listed submodule path in both base and branch: baseCommit is the commit currently recorded by the base workspace, branchCommit is the commit recorded by the worktree branch.',
|
|
1556
|
+
'Resolve the submodule first by checking out or creating the intended submodule commit, then commit the chosen gitlink in the root branch.',
|
|
1557
|
+
'Ensure the chosen submodule commit is reachable from the configured submodule remote main branch, then rerun mesh_refine_node.',
|
|
1558
|
+
],
|
|
498
1559
|
};
|
|
499
1560
|
}
|
|
500
1561
|
|
|
501
|
-
function
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
1562
|
+
function readChangedGitlinkPaths(repoRoot: string, fromRef: string, toRef: string): string[] {
|
|
1563
|
+
try {
|
|
1564
|
+
const output = execFileSync('git', ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
|
|
1565
|
+
cwd: repoRoot,
|
|
1566
|
+
encoding: 'utf8',
|
|
1567
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1568
|
+
});
|
|
1569
|
+
const paths = new Set<string>();
|
|
1570
|
+
for (const line of output.split('\n')) {
|
|
1571
|
+
if (!line.trim()) continue;
|
|
1572
|
+
const metaAndPath = line.split('\t');
|
|
1573
|
+
const meta = metaAndPath[0] || '';
|
|
1574
|
+
const path = metaAndPath[metaAndPath.length - 1]?.trim();
|
|
1575
|
+
if (!path) continue;
|
|
1576
|
+
const parts = meta.split(/\s+/);
|
|
1577
|
+
if (parts[0]?.includes('160000') || parts[1]?.includes('160000')) {
|
|
1578
|
+
paths.add(path);
|
|
1579
|
+
}
|
|
515
1580
|
}
|
|
1581
|
+
return [...paths].sort();
|
|
1582
|
+
} catch {
|
|
1583
|
+
return [];
|
|
516
1584
|
}
|
|
517
|
-
return candidates.sort((a, b) => {
|
|
518
|
-
const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
|
|
519
|
-
return rank(a.confidence) - rank(b.confidence);
|
|
520
|
-
});
|
|
521
1585
|
}
|
|
522
1586
|
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
|
|
536
|
-
return { command: commandText, category, source: 'mesh.policy.validationCommands' };
|
|
537
|
-
})
|
|
538
|
-
.filter((entry: any) => !!entry.category);
|
|
1587
|
+
function readTreeObject(repoRoot: string, ref: string, path: string): string | undefined {
|
|
1588
|
+
try {
|
|
1589
|
+
const output = execFileSync('git', ['ls-tree', ref, '--', path], {
|
|
1590
|
+
cwd: repoRoot,
|
|
1591
|
+
encoding: 'utf8',
|
|
1592
|
+
maxBuffer: 1024 * 1024,
|
|
1593
|
+
}).trim();
|
|
1594
|
+
const match = output.match(/\bcommit\s+([0-9a-f]{40})\b/i);
|
|
1595
|
+
return match?.[1];
|
|
1596
|
+
} catch {
|
|
1597
|
+
return undefined;
|
|
1598
|
+
}
|
|
539
1599
|
}
|
|
540
1600
|
|
|
541
|
-
function
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1601
|
+
async function alignRefinerySubmodulesAfterMerge(
|
|
1602
|
+
repoRoot: string,
|
|
1603
|
+
previousBaseHead: string,
|
|
1604
|
+
currentHead: string,
|
|
1605
|
+
options: { submoduleIgnorePaths?: string[] } = {},
|
|
1606
|
+
): Promise<MeshRefineSubmoduleAlignmentSummary> {
|
|
1607
|
+
const startedAt = Date.now();
|
|
1608
|
+
const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead)
|
|
1609
|
+
.filter(path => !(options.submoduleIgnorePaths || []).includes(path));
|
|
1610
|
+
const preStatus = await getGitRepoStatus(repoRoot, {
|
|
1611
|
+
includeSubmodules: true,
|
|
1612
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1613
|
+
timeoutMs: 15_000,
|
|
1614
|
+
});
|
|
1615
|
+
const outOfSyncPaths = (preStatus.submodules || [])
|
|
1616
|
+
.filter(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error)
|
|
1617
|
+
.map(submodule => submodule.path);
|
|
1618
|
+
const updatePaths = [...new Set([...changedGitlinkPaths, ...outOfSyncPaths])].sort();
|
|
1619
|
+
|
|
1620
|
+
if (updatePaths.length === 0) {
|
|
1621
|
+
return {
|
|
1622
|
+
status: 'skipped',
|
|
1623
|
+
changedGitlinkPaths,
|
|
1624
|
+
outOfSyncPaths,
|
|
1625
|
+
updatedPaths: [],
|
|
1626
|
+
verifiedPaths: [],
|
|
1627
|
+
durationMs: Date.now() - startedAt,
|
|
1628
|
+
reason: 'no_changed_or_out_of_sync_submodules',
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const commandArgs = ['submodule', 'update', '--init', '--recursive', '--', ...updatePaths];
|
|
1633
|
+
try {
|
|
1634
|
+
const { execFile } = await import('node:child_process');
|
|
1635
|
+
const { promisify } = await import('node:util');
|
|
1636
|
+
const execFileAsync = promisify(execFile);
|
|
1637
|
+
const result = await execFileAsync('git', commandArgs, {
|
|
1638
|
+
cwd: repoRoot,
|
|
1639
|
+
encoding: 'utf8',
|
|
1640
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1641
|
+
timeout: 60_000,
|
|
1642
|
+
});
|
|
1643
|
+
const postStatus = await getGitRepoStatus(repoRoot, {
|
|
1644
|
+
includeSubmodules: true,
|
|
1645
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1646
|
+
timeoutMs: 15_000,
|
|
1647
|
+
});
|
|
1648
|
+
const remaining = (postStatus.submodules || [])
|
|
1649
|
+
.filter(submodule => updatePaths.includes(submodule.path) && (submodule.dirty || submodule.outOfSync || !!submodule.error));
|
|
1650
|
+
return {
|
|
1651
|
+
status: remaining.length === 0 ? 'passed' : 'failed',
|
|
1652
|
+
changedGitlinkPaths,
|
|
1653
|
+
outOfSyncPaths,
|
|
1654
|
+
updatedPaths: updatePaths,
|
|
1655
|
+
verifiedPaths: updatePaths.filter(path => !remaining.some(submodule => submodule.path === path)),
|
|
1656
|
+
durationMs: Date.now() - startedAt,
|
|
1657
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1658
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
1659
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
1660
|
+
...(remaining.length > 0 ? { error: `Submodule checkout remained out of sync after update: ${remaining.map(entry => entry.path).join(', ')}` } : {}),
|
|
1661
|
+
};
|
|
1662
|
+
} catch (e: any) {
|
|
1663
|
+
return {
|
|
1664
|
+
status: 'failed',
|
|
1665
|
+
changedGitlinkPaths,
|
|
1666
|
+
outOfSyncPaths,
|
|
1667
|
+
updatedPaths: updatePaths,
|
|
1668
|
+
verifiedPaths: [],
|
|
1669
|
+
durationMs: Date.now() - startedAt,
|
|
1670
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1671
|
+
error: e?.message || String(e),
|
|
1672
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1673
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
async function runMeshRefineSubmoduleReachabilityGate(
|
|
1679
|
+
repoRoot: string,
|
|
1680
|
+
mergedTree: string,
|
|
1681
|
+
options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
|
|
1682
|
+
): Promise<MeshRefineSubmoduleReachabilitySummary> {
|
|
1683
|
+
const startedAt = Date.now();
|
|
1684
|
+
const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
|
|
1685
|
+
try {
|
|
1686
|
+
const { execFile } = await import('node:child_process');
|
|
1687
|
+
const { promisify } = await import('node:util');
|
|
1688
|
+
const execFileAsync = promisify(execFile);
|
|
1689
|
+
const runGit = async (cwd: string, args: string[]): Promise<string> => {
|
|
1690
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
1691
|
+
cwd,
|
|
1692
|
+
encoding: 'utf8',
|
|
1693
|
+
timeout: 30_000,
|
|
1694
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1695
|
+
windowsHide: true,
|
|
1696
|
+
});
|
|
1697
|
+
return String(stdout || '');
|
|
1698
|
+
};
|
|
1699
|
+
const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
|
|
1700
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1701
|
+
await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
|
|
1702
|
+
};
|
|
1703
|
+
const publishCommitToRemoteMain = async (submodulePath: string, commit: string, branch = 'main'): Promise<{ stdout: string; stderr: string; refspec: string }> => {
|
|
1704
|
+
const refspec = `${commit}:refs/heads/${branch}`;
|
|
1705
|
+
const { stdout, stderr } = await execFileAsync('git', ['push', 'origin', refspec], {
|
|
1706
|
+
cwd: submodulePath,
|
|
1707
|
+
encoding: 'utf8',
|
|
1708
|
+
timeout: 30_000,
|
|
1709
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1710
|
+
windowsHide: true,
|
|
1711
|
+
});
|
|
1712
|
+
return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
|
|
1713
|
+
};
|
|
1714
|
+
const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
|
|
1715
|
+
if (!fs.existsSync(worktreeSubmodulePath)) return false;
|
|
1716
|
+
try {
|
|
1717
|
+
await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1718
|
+
} catch {
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
|
|
1722
|
+
await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1723
|
+
return true;
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1727
|
+
const gitlinks = treeOutput
|
|
1728
|
+
.split('\0')
|
|
1729
|
+
.filter(Boolean)
|
|
1730
|
+
.map(record => {
|
|
1731
|
+
const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
1732
|
+
return match ? { commit: match[1], path: match[2] } : null;
|
|
1733
|
+
})
|
|
1734
|
+
.filter((entry): entry is { commit: string; path: string } => !!entry);
|
|
1735
|
+
|
|
1736
|
+
for (const gitlink of gitlinks) {
|
|
1737
|
+
const submodulePath = pathResolve(repoRoot, gitlink.path);
|
|
1738
|
+
const entry: MeshRefineSubmoduleReachabilityEntry = {
|
|
1739
|
+
path: gitlink.path,
|
|
1740
|
+
commit: gitlink.commit,
|
|
1741
|
+
reachable: false,
|
|
1742
|
+
};
|
|
1743
|
+
try {
|
|
1744
|
+
if (!fs.existsSync(submodulePath)) {
|
|
1745
|
+
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1746
|
+
entry.publishRequired = true;
|
|
1747
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1748
|
+
entry.autoPublishAllowed = true;
|
|
1749
|
+
entry.autoPublishAttempted = false;
|
|
1750
|
+
entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
|
|
1751
|
+
}
|
|
1752
|
+
entries.push(entry);
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
entry.checkedLocal = true;
|
|
1757
|
+
try {
|
|
1758
|
+
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1759
|
+
entry.localReachable = true;
|
|
1760
|
+
} catch {
|
|
1761
|
+
entry.localReachable = false;
|
|
1762
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
|
|
1763
|
+
try {
|
|
1764
|
+
const imported = await importCommitFromWorktreeSubmodule(
|
|
1765
|
+
submodulePath,
|
|
1766
|
+
pathResolve(options.worktreeRoot, gitlink.path),
|
|
1767
|
+
gitlink.commit,
|
|
1768
|
+
);
|
|
1769
|
+
if (imported) {
|
|
1770
|
+
entry.localReachable = true;
|
|
1771
|
+
entry.importedFromWorktree = true;
|
|
1772
|
+
}
|
|
1773
|
+
} catch (importError: any) {
|
|
1774
|
+
entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
// Probe the submodule remote before allowing cleanup/completion.
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
try {
|
|
1781
|
+
entry.remote = 'origin';
|
|
1782
|
+
let remoteUrl = '';
|
|
1783
|
+
try {
|
|
1784
|
+
remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
|
|
1785
|
+
if (!remoteUrl) throw new Error('origin remote has no URL');
|
|
1786
|
+
entry.remoteUrl = remoteUrl;
|
|
1787
|
+
} catch {
|
|
1788
|
+
entry.error = 'Submodule remote reachability check failed: no configured origin remote';
|
|
1789
|
+
entry.publishRequired = true;
|
|
1790
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1791
|
+
entry.autoPublishAllowed = true;
|
|
1792
|
+
entry.autoPublishAttempted = false;
|
|
1793
|
+
entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
|
|
1794
|
+
}
|
|
1795
|
+
entries.push(entry);
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
entry.remoteMainBranch = 'main';
|
|
1799
|
+
try {
|
|
1800
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1801
|
+
entry.fetchedFromOrigin = true;
|
|
1802
|
+
entry.remoteReachable = true;
|
|
1803
|
+
entry.remoteMainReachable = true;
|
|
1804
|
+
entry.reachable = true;
|
|
1805
|
+
} catch (e: any) {
|
|
1806
|
+
entry.remoteReachable = false;
|
|
1807
|
+
entry.remoteMainReachable = false;
|
|
1808
|
+
entry.publishRequired = true;
|
|
1809
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1810
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1811
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
|
|
1812
|
+
entry.autoPublishAllowed = true;
|
|
1813
|
+
entry.autoPublishAttempted = true;
|
|
1814
|
+
try {
|
|
1815
|
+
const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, 'main');
|
|
1816
|
+
entry.autoPublishRefspec = publish.refspec;
|
|
1817
|
+
entry.publishStdout = truncateValidationOutput(publish.stdout);
|
|
1818
|
+
entry.publishStderr = truncateValidationOutput(publish.stderr);
|
|
1819
|
+
entry.autoPublishSucceeded = true;
|
|
1820
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1821
|
+
entry.fetchedFromOrigin = true;
|
|
1822
|
+
entry.remoteReachable = true;
|
|
1823
|
+
entry.remoteMainReachable = true;
|
|
1824
|
+
entry.autoPublishVerified = true;
|
|
1825
|
+
entry.publishRequired = false;
|
|
1826
|
+
entry.reachable = true;
|
|
1827
|
+
entry.error = undefined;
|
|
1828
|
+
} catch (publishError: any) {
|
|
1829
|
+
entry.autoPublishSucceeded = false;
|
|
1830
|
+
entry.autoPublishVerified = false;
|
|
1831
|
+
const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
|
|
1832
|
+
entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
|
|
1833
|
+
}
|
|
1834
|
+
} else if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1835
|
+
entry.autoPublishAllowed = true;
|
|
1836
|
+
entry.autoPublishAttempted = false;
|
|
1837
|
+
entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
|
|
1838
|
+
|| 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
} catch (e: any) {
|
|
1842
|
+
entry.remoteReachable = false;
|
|
1843
|
+
entry.remoteMainReachable = false;
|
|
1844
|
+
entry.publishRequired = true;
|
|
1845
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1846
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1847
|
+
}
|
|
1848
|
+
} catch (e: any) {
|
|
1849
|
+
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
1850
|
+
entry.publishRequired = true;
|
|
1851
|
+
}
|
|
1852
|
+
entries.push(entry);
|
|
574
1853
|
}
|
|
1854
|
+
|
|
1855
|
+
const unreachable = entries.filter(entry => !entry.reachable);
|
|
1856
|
+
return {
|
|
1857
|
+
status: unreachable.length ? 'failed' : 'passed',
|
|
1858
|
+
checked: entries.length,
|
|
1859
|
+
unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
|
|
1860
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
|
|
1861
|
+
durationMs: Date.now() - startedAt,
|
|
1862
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1863
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1864
|
+
};
|
|
1865
|
+
} catch (e: any) {
|
|
1866
|
+
const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
|
|
1867
|
+
return {
|
|
1868
|
+
status: 'failed',
|
|
1869
|
+
checked: entries.length,
|
|
1870
|
+
unreachable,
|
|
1871
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
|
|
1872
|
+
durationMs: Date.now() - startedAt,
|
|
1873
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1874
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1875
|
+
error: truncateValidationOutput(e?.message || String(e)),
|
|
1876
|
+
};
|
|
575
1877
|
}
|
|
1878
|
+
}
|
|
576
1879
|
|
|
1880
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1881
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1882
|
+
const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
|
|
1883
|
+
displayCommand: command.displayCommand,
|
|
1884
|
+
category: command.category,
|
|
1885
|
+
source: command.source,
|
|
1886
|
+
cwd: command.cwd,
|
|
1887
|
+
timeoutMs: command.timeoutMs,
|
|
1888
|
+
});
|
|
577
1889
|
return {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1890
|
+
source: plan.source,
|
|
1891
|
+
sourceType: plan.sourceType,
|
|
1892
|
+
bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
|
|
1893
|
+
commands: plan.commands.map(mapCommand),
|
|
1894
|
+
unavailableReason: plan.unavailableReason,
|
|
1895
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1896
|
+
suggestions: plan.suggestions,
|
|
1897
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1898
|
+
note: plan.sourceType === 'unavailable'
|
|
1899
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1900
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
587
1901
|
};
|
|
588
1902
|
}
|
|
589
1903
|
|
|
@@ -591,60 +1905,119 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
591
1905
|
const { execFile } = await import('node:child_process');
|
|
592
1906
|
const { promisify } = await import('node:util');
|
|
593
1907
|
const execFileAsync = promisify(execFile);
|
|
594
|
-
const selection =
|
|
1908
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
595
1909
|
const summary: MeshRefineValidationSummary = {
|
|
596
1910
|
status: 'skipped',
|
|
597
1911
|
required: true,
|
|
598
1912
|
commandsRun: [],
|
|
1913
|
+
bootstrapCommandsRun: [],
|
|
599
1914
|
rejectedCommands: selection.rejectedCommands,
|
|
600
1915
|
skippedReason: undefined,
|
|
601
1916
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
602
1917
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1918
|
+
configSource: selection.source,
|
|
1919
|
+
configSourceType: selection.sourceType,
|
|
1920
|
+
suggestions: selection.suggestions,
|
|
1921
|
+
suggestedConfig: selection.suggestedConfig,
|
|
603
1922
|
};
|
|
604
1923
|
|
|
605
1924
|
if (!selection.commands.length) {
|
|
606
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1925
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
607
1926
|
return summary;
|
|
608
1927
|
}
|
|
609
1928
|
|
|
1929
|
+
const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
|
|
1930
|
+
command: candidate.command,
|
|
1931
|
+
args: candidate.args,
|
|
1932
|
+
displayCommand: candidate.displayCommand,
|
|
1933
|
+
category: candidate.category,
|
|
1934
|
+
source: candidate.source,
|
|
1935
|
+
cwd,
|
|
1936
|
+
passed,
|
|
1937
|
+
durationMs: Date.now() - startedAt,
|
|
1938
|
+
stdout: truncateValidationOutput(result?.stdout),
|
|
1939
|
+
stderr: truncateValidationOutput(result?.stderr || result?.message),
|
|
1940
|
+
...extras,
|
|
1941
|
+
});
|
|
1942
|
+
const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
|
|
1943
|
+
const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
|
|
1944
|
+
return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
|
|
1945
|
+
&& candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
|
|
1946
|
+
};
|
|
1947
|
+
const dependenciesLikelyMissing = (cwd: string): boolean => {
|
|
1948
|
+
if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
|
|
1949
|
+
if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
|
|
1950
|
+
return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
|
|
1951
|
+
.some(lock => fs.existsSync(pathJoin(cwd, lock)));
|
|
1952
|
+
};
|
|
1953
|
+
|
|
1954
|
+
for (const candidate of selection.bootstrapCommands) {
|
|
1955
|
+
const startedAt = Date.now();
|
|
1956
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1957
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1958
|
+
try {
|
|
1959
|
+
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
1960
|
+
cwd,
|
|
1961
|
+
encoding: 'utf8',
|
|
1962
|
+
timeout,
|
|
1963
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1964
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1965
|
+
});
|
|
1966
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1967
|
+
} catch (error: any) {
|
|
1968
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
1969
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
1970
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
1971
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
1972
|
+
failureKind: 'dependency_bootstrap_failed',
|
|
1973
|
+
}));
|
|
1974
|
+
summary.status = 'failed';
|
|
1975
|
+
summary.failureKind = 'dependency_bootstrap_failed';
|
|
1976
|
+
summary.failureCode = 'dependency_bootstrap_failed';
|
|
1977
|
+
return summary;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
610
1981
|
for (const candidate of selection.commands) {
|
|
611
1982
|
const startedAt = Date.now();
|
|
1983
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1984
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1985
|
+
if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
|
|
1986
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
|
|
1987
|
+
stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
|
|
1988
|
+
}, false, {
|
|
1989
|
+
exitCode: null,
|
|
1990
|
+
skipped: true,
|
|
1991
|
+
failureKind: 'missing_dependencies',
|
|
1992
|
+
}));
|
|
1993
|
+
summary.status = 'failed';
|
|
1994
|
+
summary.failureKind = 'missing_dependencies';
|
|
1995
|
+
summary.failureCode = 'missing_dependencies';
|
|
1996
|
+
return summary;
|
|
1997
|
+
}
|
|
612
1998
|
try {
|
|
613
1999
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
614
|
-
cwd
|
|
2000
|
+
cwd,
|
|
615
2001
|
encoding: 'utf8',
|
|
616
|
-
timeout
|
|
617
|
-
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
618
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
619
|
-
});
|
|
620
|
-
summary.commandsRun.push({
|
|
621
|
-
command: candidate.command,
|
|
622
|
-
args: candidate.args,
|
|
623
|
-
displayCommand: candidate.displayCommand,
|
|
624
|
-
category: candidate.category,
|
|
625
|
-
source: candidate.source,
|
|
626
|
-
passed: true,
|
|
627
|
-
exitCode: 0,
|
|
628
|
-
durationMs: Date.now() - startedAt,
|
|
629
|
-
stdout: truncateValidationOutput(result.stdout),
|
|
630
|
-
stderr: truncateValidationOutput(result.stderr),
|
|
2002
|
+
timeout,
|
|
2003
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
2004
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
631
2005
|
});
|
|
2006
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
632
2007
|
} catch (error: any) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
displayCommand: candidate.displayCommand,
|
|
637
|
-
category: candidate.category,
|
|
638
|
-
source: candidate.source,
|
|
639
|
-
passed: false,
|
|
2008
|
+
const stderr = truncateValidationOutput(error?.stderr || error?.message);
|
|
2009
|
+
const missingDependencyFailure = /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
|
|
2010
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
640
2011
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
641
2012
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
642
2013
|
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
stderr: truncateValidationOutput(error?.stderr || error?.message),
|
|
646
|
-
});
|
|
2014
|
+
...(missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
|
|
2015
|
+
}));
|
|
647
2016
|
summary.status = 'failed';
|
|
2017
|
+
if (missingDependencyFailure) {
|
|
2018
|
+
summary.failureKind = 'missing_dependencies';
|
|
2019
|
+
summary.failureCode = 'missing_dependencies';
|
|
2020
|
+
}
|
|
648
2021
|
return summary;
|
|
649
2022
|
}
|
|
650
2023
|
}
|
|
@@ -776,6 +2149,8 @@ export interface CommandRouterDeps {
|
|
|
776
2149
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
777
2150
|
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
778
2151
|
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
2152
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
2153
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
779
2154
|
}
|
|
780
2155
|
|
|
781
2156
|
export interface CommandRouterResult {
|
|
@@ -887,42 +2262,266 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
887
2262
|
};
|
|
888
2263
|
}
|
|
889
2264
|
|
|
2265
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
2266
|
+
const raw = hostAddress.trim();
|
|
2267
|
+
if (!raw) throw new Error('hostAddress required');
|
|
2268
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
2269
|
+
url.pathname = '/api/v1/command';
|
|
2270
|
+
url.search = '';
|
|
2271
|
+
url.hash = '';
|
|
2272
|
+
return url.toString();
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
2276
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
2277
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
2278
|
+
? args.memberNode as Record<string, any>
|
|
2279
|
+
: null;
|
|
2280
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
2281
|
+
? (requestedNodeId
|
|
2282
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
2283
|
+
: mesh.nodes[0])
|
|
2284
|
+
: null;
|
|
2285
|
+
const source = explicit || configured;
|
|
2286
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
2287
|
+
? source.workspace.trim()
|
|
2288
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
2289
|
+
? args.workspace.trim()
|
|
2290
|
+
: process.cwd();
|
|
2291
|
+
if (!workspace) return null;
|
|
2292
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
2293
|
+
? source.id.trim()
|
|
2294
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
2295
|
+
? source.nodeId.trim()
|
|
2296
|
+
: undefined;
|
|
2297
|
+
return {
|
|
2298
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
2299
|
+
workspace,
|
|
2300
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
2301
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
2302
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
2303
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
2304
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
2305
|
+
role: 'member',
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
|
|
890
2309
|
export class DaemonCommandRouter {
|
|
891
2310
|
private deps: CommandRouterDeps;
|
|
892
2311
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
893
2312
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
894
2313
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
895
2314
|
private inlineMeshCache = new Map<string, any>();
|
|
2315
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
2316
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
|
|
2317
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
2318
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
2319
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
2320
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
896
2321
|
|
|
897
2322
|
constructor(deps: CommandRouterDeps) {
|
|
898
2323
|
this.deps = deps;
|
|
899
2324
|
}
|
|
900
2325
|
|
|
2326
|
+
private cloneJsonValue<T>(value: T): T {
|
|
2327
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
2328
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
2332
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
2333
|
+
const inlineNodesById = new Map<string, any>();
|
|
2334
|
+
for (const node of mesh.nodes) {
|
|
2335
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
2336
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
2337
|
+
}
|
|
2338
|
+
if (!inlineNodesById.size) return snapshot;
|
|
2339
|
+
|
|
2340
|
+
let changed = false;
|
|
2341
|
+
const unavailableNodeIds = new Set<string>();
|
|
2342
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
2343
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
2344
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
2345
|
+
const nodeId = readStringValue(entry);
|
|
2346
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
2350
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
2351
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
2352
|
+
if (!inlineNode) return statusNode;
|
|
2353
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
2354
|
+
if (!liveGit) return statusNode;
|
|
2355
|
+
const nextStatus = { ...statusNode };
|
|
2356
|
+
nextStatus.git = liveGit;
|
|
2357
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
2358
|
+
applyInlineMeshBranchConvergence(mesh, inlineNode, nextStatus);
|
|
2359
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
2360
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
2361
|
+
const connectionState = readStringValue(connection.state);
|
|
2362
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
2363
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
2364
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
2365
|
+
}
|
|
2366
|
+
delete nextStatus.gitProbePending;
|
|
2367
|
+
const error = readStringValue(nextStatus.error);
|
|
2368
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
2369
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
2370
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
2371
|
+
changed = true;
|
|
2372
|
+
return nextStatus;
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
const aggregateDirectTruthSatisfied = sourceOfTruth.coordinatorOwnsLiveTruth === true
|
|
2376
|
+
|| directPeerTruth.satisfied === true;
|
|
2377
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied)) return snapshot;
|
|
2378
|
+
const nextSourceOfTruth = {
|
|
2379
|
+
...sourceOfTruth,
|
|
2380
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
2381
|
+
directPeerTruth: {
|
|
2382
|
+
...directPeerTruth,
|
|
2383
|
+
satisfied: options?.requireDirectPeerTruth === true
|
|
2384
|
+
? aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0
|
|
2385
|
+
: directPeerTruth.satisfied,
|
|
2386
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
2387
|
+
},
|
|
2388
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
2389
|
+
coordinatorOwnsLiveTruth: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0,
|
|
2390
|
+
currentStatus: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
2391
|
+
} : {}),
|
|
2392
|
+
} : {}),
|
|
2393
|
+
};
|
|
2394
|
+
return {
|
|
2395
|
+
...snapshot,
|
|
2396
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied ? {
|
|
2397
|
+
success: false,
|
|
2398
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
2399
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
2400
|
+
} : {}),
|
|
2401
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
2402
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodes),
|
|
2403
|
+
nodes,
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
2408
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
2409
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
2410
|
+
if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
|
|
2411
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
2412
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
2413
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
2414
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
2415
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
2416
|
+
? snapshot.sourceOfTruth
|
|
2417
|
+
: {};
|
|
2418
|
+
snapshot.sourceOfTruth = {
|
|
2419
|
+
...sourceOfTruth,
|
|
2420
|
+
aggregateSnapshot: {
|
|
2421
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
2422
|
+
? sourceOfTruth.aggregateSnapshot
|
|
2423
|
+
: {}),
|
|
2424
|
+
owner: 'coordinator_daemon_memory',
|
|
2425
|
+
cached: true,
|
|
2426
|
+
source: 'memory',
|
|
2427
|
+
refreshReason: 'memory_cache_hit',
|
|
2428
|
+
ageMs,
|
|
2429
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
2430
|
+
returnedAt: new Date().toISOString(),
|
|
2431
|
+
},
|
|
2432
|
+
};
|
|
2433
|
+
return snapshot;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
2437
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
2438
|
+
const builtAt = Date.now();
|
|
2439
|
+
const next = this.cloneJsonValue(snapshot);
|
|
2440
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
2441
|
+
? next.sourceOfTruth
|
|
2442
|
+
: {};
|
|
2443
|
+
next.sourceOfTruth = {
|
|
2444
|
+
...sourceOfTruth,
|
|
2445
|
+
aggregateSnapshot: {
|
|
2446
|
+
owner: 'coordinator_daemon_memory',
|
|
2447
|
+
cached: false,
|
|
2448
|
+
source: 'live_refresh',
|
|
2449
|
+
refreshReason,
|
|
2450
|
+
ageMs: 0,
|
|
2451
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
2452
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
2453
|
+
},
|
|
2454
|
+
};
|
|
2455
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
|
|
2456
|
+
return next;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
901
2459
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
902
2460
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
903
|
-
this.
|
|
904
|
-
return inlineMesh as any;
|
|
2461
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
905
2462
|
}
|
|
906
2463
|
return this.inlineMeshCache.get(meshId);
|
|
907
2464
|
}
|
|
908
2465
|
|
|
2466
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
2467
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
2468
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
2469
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
2470
|
+
if (cached) {
|
|
2471
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
2472
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
2473
|
+
return merged;
|
|
2474
|
+
}
|
|
2475
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
2476
|
+
return sanitizedInlineMesh as any;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
909
2479
|
private async getMeshForCommand(
|
|
910
2480
|
meshId: string,
|
|
911
2481
|
inlineMesh?: unknown,
|
|
912
2482
|
options?: { preferInline?: boolean },
|
|
913
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
2483
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
914
2484
|
const preferInline = options?.preferInline === true;
|
|
915
2485
|
if (preferInline) {
|
|
916
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
917
|
-
if (cached)
|
|
2486
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
2487
|
+
if (cached) {
|
|
2488
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
2489
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
2490
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
2491
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
2492
|
+
}
|
|
2493
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
2494
|
+
}
|
|
2495
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
2496
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
2497
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
2498
|
+
}
|
|
918
2499
|
}
|
|
919
2500
|
try {
|
|
920
2501
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
921
2502
|
const mesh = getMesh(meshId);
|
|
922
|
-
if (mesh) return { mesh, inline: false };
|
|
2503
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
923
2504
|
} catch { /* fall through to inline cache */ }
|
|
924
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
925
|
-
|
|
2505
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
2506
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
2507
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
2508
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
2512
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
|
|
2516
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
2517
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
2518
|
+
const mesh = meshRecord?.mesh;
|
|
2519
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2520
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
2521
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
2522
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
2523
|
+
}
|
|
2524
|
+
return null;
|
|
926
2525
|
}
|
|
927
2526
|
|
|
928
2527
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
@@ -932,6 +2531,7 @@ export class DaemonCommandRouter {
|
|
|
932
2531
|
else mesh.nodes.push(node);
|
|
933
2532
|
mesh.updatedAt = new Date().toISOString();
|
|
934
2533
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2534
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
935
2535
|
}
|
|
936
2536
|
|
|
937
2537
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -941,6 +2541,7 @@ export class DaemonCommandRouter {
|
|
|
941
2541
|
mesh.nodes.splice(idx, 1);
|
|
942
2542
|
mesh.updatedAt = new Date().toISOString();
|
|
943
2543
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2544
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
944
2545
|
return true;
|
|
945
2546
|
}
|
|
946
2547
|
|
|
@@ -1219,6 +2820,7 @@ export class DaemonCommandRouter {
|
|
|
1219
2820
|
const deletedSessionIds: string[] = [];
|
|
1220
2821
|
const skippedSessionIds: string[] = [];
|
|
1221
2822
|
const skippedLiveSessionIds: string[] = [];
|
|
2823
|
+
const skippedCoordinatorSessionIds: string[] = [];
|
|
1222
2824
|
const deleteUnsupportedSessionIds: string[] = [];
|
|
1223
2825
|
const recordsRemainSessionIds: string[] = [];
|
|
1224
2826
|
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
@@ -1253,6 +2855,12 @@ export class DaemonCommandRouter {
|
|
|
1253
2855
|
const completed = this.isCompletedHostedSession(record);
|
|
1254
2856
|
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1255
2857
|
const liveRuntime = surfaceKind === 'live_runtime';
|
|
2858
|
+
const coordinatorSession = readStringValue(record?.meta?.meshCoordinatorFor) === args.meshId;
|
|
2859
|
+
if (!hasExplicitSessionIds && coordinatorSession) {
|
|
2860
|
+
skippedSessionIds.push(sessionId);
|
|
2861
|
+
skippedCoordinatorSessionIds.push(sessionId);
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
1256
2864
|
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1257
2865
|
skippedSessionIds.push(sessionId);
|
|
1258
2866
|
skippedLiveSessionIds.push(sessionId);
|
|
@@ -1322,6 +2930,7 @@ export class DaemonCommandRouter {
|
|
|
1322
2930
|
deletedSessionIds,
|
|
1323
2931
|
skippedSessionIds,
|
|
1324
2932
|
skippedLiveSessionIds,
|
|
2933
|
+
skippedCoordinatorSessionIds,
|
|
1325
2934
|
...(deleteUnsupported ? {
|
|
1326
2935
|
deleteUnsupported: true,
|
|
1327
2936
|
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
@@ -1462,6 +3071,567 @@ export class DaemonCommandRouter {
|
|
|
1462
3071
|
}
|
|
1463
3072
|
}
|
|
1464
3073
|
|
|
3074
|
+
|
|
3075
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
3076
|
+
return `${meshId}:${nodeId}`;
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
private buildRefineJobHandle(args: {
|
|
3080
|
+
meshId: string;
|
|
3081
|
+
nodeId: string;
|
|
3082
|
+
node?: any;
|
|
3083
|
+
status?: MeshRefineAsyncJobStatus;
|
|
3084
|
+
startedAt?: string;
|
|
3085
|
+
completedAt?: string;
|
|
3086
|
+
jobId?: string;
|
|
3087
|
+
interactionId?: string;
|
|
3088
|
+
retryOfJobId?: string;
|
|
3089
|
+
}): MeshRefineJobHandle {
|
|
3090
|
+
return {
|
|
3091
|
+
success: true,
|
|
3092
|
+
async: true,
|
|
3093
|
+
status: args.status || 'accepted',
|
|
3094
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
3095
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
3096
|
+
meshId: args.meshId,
|
|
3097
|
+
nodeId: args.nodeId,
|
|
3098
|
+
targetNodeId: args.nodeId,
|
|
3099
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
3100
|
+
workspace: readStringValue(args.node?.workspace),
|
|
3101
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
3102
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
3103
|
+
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
3104
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
3105
|
+
evidence: {
|
|
3106
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
3107
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
3108
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
3109
|
+
},
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
3114
|
+
const metadataEvent = {
|
|
3115
|
+
source: 'refine_mesh_node_async_job',
|
|
3116
|
+
jobId: handle.jobId,
|
|
3117
|
+
interactionId: handle.interactionId,
|
|
3118
|
+
meshId: handle.meshId,
|
|
3119
|
+
nodeId: handle.targetNodeId,
|
|
3120
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3121
|
+
workspace: handle.workspace,
|
|
3122
|
+
status: handle.status,
|
|
3123
|
+
startedAt: handle.startedAt,
|
|
3124
|
+
completedAt: handle.completedAt,
|
|
3125
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3126
|
+
...(result ? { result } : {}),
|
|
3127
|
+
};
|
|
3128
|
+
const eventPayload = {
|
|
3129
|
+
event,
|
|
3130
|
+
meshId: handle.meshId,
|
|
3131
|
+
nodeLabel: handle.targetNodeId,
|
|
3132
|
+
nodeId: handle.targetNodeId,
|
|
3133
|
+
workspace: handle.workspace,
|
|
3134
|
+
metadataEvent,
|
|
3135
|
+
queuedAt: Date.now(),
|
|
3136
|
+
};
|
|
3137
|
+
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
3138
|
+
const forwarded = handleMeshForwardEvent(
|
|
3139
|
+
{ instanceManager: this.deps.instanceManager } as any,
|
|
3140
|
+
{
|
|
3141
|
+
event,
|
|
3142
|
+
meshId: handle.meshId,
|
|
3143
|
+
nodeId: handle.targetNodeId,
|
|
3144
|
+
workspace: handle.workspace,
|
|
3145
|
+
jobId: handle.jobId,
|
|
3146
|
+
interactionId: handle.interactionId,
|
|
3147
|
+
status: handle.status,
|
|
3148
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3149
|
+
startedAt: handle.startedAt,
|
|
3150
|
+
completedAt: handle.completedAt,
|
|
3151
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3152
|
+
...(result ? { result } : {}),
|
|
3153
|
+
},
|
|
3154
|
+
);
|
|
3155
|
+
if (forwarded?.success === true) return;
|
|
3156
|
+
LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
|
|
3157
|
+
}
|
|
3158
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
3162
|
+
try {
|
|
3163
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3164
|
+
appendLedgerEntry(handle.meshId, {
|
|
3165
|
+
kind,
|
|
3166
|
+
nodeId: handle.targetNodeId,
|
|
3167
|
+
payload: {
|
|
3168
|
+
source: 'refine_mesh_node_async_job',
|
|
3169
|
+
refineJob: {
|
|
3170
|
+
jobId: handle.jobId,
|
|
3171
|
+
interactionId: handle.interactionId,
|
|
3172
|
+
status: handle.status,
|
|
3173
|
+
meshId: handle.meshId,
|
|
3174
|
+
nodeId: handle.targetNodeId,
|
|
3175
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3176
|
+
workspace: handle.workspace,
|
|
3177
|
+
startedAt: handle.startedAt,
|
|
3178
|
+
completedAt: handle.completedAt,
|
|
3179
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3180
|
+
},
|
|
3181
|
+
async: true,
|
|
3182
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3183
|
+
...(result ? {
|
|
3184
|
+
success: result.success === true,
|
|
3185
|
+
result,
|
|
3186
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
3187
|
+
} : {}),
|
|
3188
|
+
},
|
|
3189
|
+
});
|
|
3190
|
+
} catch (e: any) {
|
|
3191
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
3196
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
3197
|
+
try {
|
|
3198
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3199
|
+
const mesh = meshRecord?.mesh;
|
|
3200
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3201
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
3202
|
+
|
|
3203
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
3204
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
const sourceNode = node.clonedFromNodeId
|
|
3208
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
3209
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
3210
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
3211
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
3212
|
+
|
|
3213
|
+
const { execFile } = await import('node:child_process');
|
|
3214
|
+
const { promisify } = await import('node:util');
|
|
3215
|
+
const execFileAsync = promisify(execFile);
|
|
3216
|
+
|
|
3217
|
+
const resolveStarted = Date.now();
|
|
3218
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
3219
|
+
const branch = branchStdout.trim();
|
|
3220
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
3221
|
+
|
|
3222
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3223
|
+
const baseBranch = baseBranchStdout.trim();
|
|
3224
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3225
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
3226
|
+
const baseHead = baseHeadStdout.trim();
|
|
3227
|
+
const branchHead = branchHeadStdout.trim();
|
|
3228
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
3229
|
+
|
|
3230
|
+
const validationStarted = Date.now();
|
|
3231
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
3232
|
+
recordMeshRefineStage(
|
|
3233
|
+
refineStages,
|
|
3234
|
+
'validation',
|
|
3235
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
3236
|
+
validationStarted,
|
|
3237
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
3238
|
+
);
|
|
3239
|
+
if (validationSummary.status === 'failed') {
|
|
3240
|
+
return {
|
|
3241
|
+
success: false,
|
|
3242
|
+
code: validationSummary.failureCode || 'validation_failed',
|
|
3243
|
+
convergenceStatus: 'blocked_review',
|
|
3244
|
+
error: validationSummary.failureCode === 'missing_dependencies'
|
|
3245
|
+
? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
|
|
3246
|
+
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
3247
|
+
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
3248
|
+
: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
3249
|
+
branch,
|
|
3250
|
+
into: baseBranch,
|
|
3251
|
+
validationSummary,
|
|
3252
|
+
refineStages,
|
|
3253
|
+
finalBranchConvergenceState: {
|
|
3254
|
+
branch,
|
|
3255
|
+
baseBranch,
|
|
3256
|
+
merged: false,
|
|
3257
|
+
removed: false,
|
|
3258
|
+
validation: 'failed',
|
|
3259
|
+
status: 'blocked_review',
|
|
3260
|
+
},
|
|
3261
|
+
};
|
|
3262
|
+
}
|
|
3263
|
+
if (validationSummary.status === 'skipped') {
|
|
3264
|
+
return {
|
|
3265
|
+
success: false,
|
|
3266
|
+
code: 'validation_unavailable',
|
|
3267
|
+
convergenceStatus: 'blocked_review',
|
|
3268
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
3269
|
+
branch,
|
|
3270
|
+
into: baseBranch,
|
|
3271
|
+
validationSummary,
|
|
3272
|
+
refineStages,
|
|
3273
|
+
finalBranchConvergenceState: {
|
|
3274
|
+
branch,
|
|
3275
|
+
baseBranch,
|
|
3276
|
+
merged: false,
|
|
3277
|
+
removed: false,
|
|
3278
|
+
validation: 'unavailable',
|
|
3279
|
+
status: 'blocked_review',
|
|
3280
|
+
},
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
const patchEquivalenceStarted = Date.now();
|
|
3285
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3286
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
3287
|
+
equivalent: patchEquivalence.equivalent,
|
|
3288
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
3289
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
3290
|
+
error: patchEquivalence.error,
|
|
3291
|
+
actionableHint: patchEquivalence.actionableHint,
|
|
3292
|
+
});
|
|
3293
|
+
if (!patchEquivalence.equivalent) {
|
|
3294
|
+
return {
|
|
3295
|
+
success: false,
|
|
3296
|
+
code: 'patch_equivalence_failed',
|
|
3297
|
+
convergenceStatus: 'blocked_review',
|
|
3298
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
3299
|
+
branch,
|
|
3300
|
+
into: baseBranch,
|
|
3301
|
+
validationSummary,
|
|
3302
|
+
patchEquivalence,
|
|
3303
|
+
refineStages,
|
|
3304
|
+
finalBranchConvergenceState: {
|
|
3305
|
+
branch,
|
|
3306
|
+
baseBranch,
|
|
3307
|
+
merged: false,
|
|
3308
|
+
removed: false,
|
|
3309
|
+
validation: 'passed',
|
|
3310
|
+
patchEquivalence: 'failed',
|
|
3311
|
+
status: 'blocked_review',
|
|
3312
|
+
},
|
|
3313
|
+
};
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
const submoduleReachabilityStarted = Date.now();
|
|
3317
|
+
const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
|
|
3318
|
+
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
3319
|
+
allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
|
|
3320
|
+
autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
|
|
3321
|
+
worktreeRoot: node.workspace,
|
|
3322
|
+
});
|
|
3323
|
+
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
3324
|
+
checked: submoduleReachability.checked,
|
|
3325
|
+
autoPublishAllowed: submoduleReachability.autoPublishAllowed,
|
|
3326
|
+
autoPublishPolicySource: submoduleReachability.autoPublishPolicySource,
|
|
3327
|
+
autoPublished: submoduleReachability.entries
|
|
3328
|
+
.filter(entry => entry.autoPublishAttempted)
|
|
3329
|
+
.map(entry => ({
|
|
3330
|
+
path: entry.path,
|
|
3331
|
+
commit: entry.commit,
|
|
3332
|
+
remote: entry.remote,
|
|
3333
|
+
remoteUrl: entry.remoteUrl,
|
|
3334
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3335
|
+
refspec: entry.autoPublishRefspec,
|
|
3336
|
+
succeeded: entry.autoPublishSucceeded,
|
|
3337
|
+
verified: entry.autoPublishVerified,
|
|
3338
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3339
|
+
error: entry.error,
|
|
3340
|
+
})),
|
|
3341
|
+
autoPublishSkipped: submoduleReachability.entries
|
|
3342
|
+
.filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
|
|
3343
|
+
.map(entry => ({
|
|
3344
|
+
path: entry.path,
|
|
3345
|
+
commit: entry.commit,
|
|
3346
|
+
remote: entry.remote,
|
|
3347
|
+
remoteUrl: entry.remoteUrl,
|
|
3348
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3349
|
+
reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
|
|
3350
|
+
})),
|
|
3351
|
+
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
3352
|
+
path: entry.path,
|
|
3353
|
+
commit: entry.commit,
|
|
3354
|
+
publishRequired: entry.publishRequired === true,
|
|
3355
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
3356
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
3357
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
3358
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
3359
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3360
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
3361
|
+
remote: entry.remote,
|
|
3362
|
+
remoteUrl: entry.remoteUrl,
|
|
3363
|
+
remoteReachable: entry.remoteReachable,
|
|
3364
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3365
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3366
|
+
error: entry.error,
|
|
3367
|
+
})),
|
|
3368
|
+
error: submoduleReachability.error,
|
|
3369
|
+
});
|
|
3370
|
+
if (submoduleReachability.status === 'failed') {
|
|
3371
|
+
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
3372
|
+
return {
|
|
3373
|
+
success: false,
|
|
3374
|
+
code: 'submodule_reachability_failed',
|
|
3375
|
+
convergenceStatus: 'blocked_review',
|
|
3376
|
+
publishRequired: true,
|
|
3377
|
+
blockedReason: 'submodule_publish_required',
|
|
3378
|
+
error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
|
|
3379
|
+
nextStep,
|
|
3380
|
+
nextSteps: [
|
|
3381
|
+
'Ask the user for explicit approval before pushing or publishing any submodule commit.',
|
|
3382
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
|
|
3383
|
+
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
3384
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
|
|
3385
|
+
],
|
|
3386
|
+
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
3387
|
+
path: entry.path,
|
|
3388
|
+
commit: entry.commit,
|
|
3389
|
+
remote: entry.remote,
|
|
3390
|
+
remoteUrl: entry.remoteUrl,
|
|
3391
|
+
remoteReachable: entry.remoteReachable,
|
|
3392
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3393
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3394
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
3395
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
3396
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
3397
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
3398
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3399
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
3400
|
+
error: entry.error,
|
|
3401
|
+
})),
|
|
3402
|
+
branch,
|
|
3403
|
+
into: baseBranch,
|
|
3404
|
+
validationSummary,
|
|
3405
|
+
patchEquivalence,
|
|
3406
|
+
submoduleReachability,
|
|
3407
|
+
refineStages,
|
|
3408
|
+
finalBranchConvergenceState: {
|
|
3409
|
+
branch,
|
|
3410
|
+
baseBranch,
|
|
3411
|
+
merged: false,
|
|
3412
|
+
removed: false,
|
|
3413
|
+
validation: 'passed',
|
|
3414
|
+
patchEquivalence: 'passed',
|
|
3415
|
+
submoduleReachability: 'failed',
|
|
3416
|
+
status: 'blocked_review',
|
|
3417
|
+
reason: 'submodule_publish_required',
|
|
3418
|
+
nextStep,
|
|
3419
|
+
},
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
3424
|
+
const mergeStarted = Date.now();
|
|
3425
|
+
try {
|
|
3426
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3427
|
+
mergeResult = {
|
|
3428
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
3429
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
3430
|
+
durationMs: Date.now() - mergeStarted,
|
|
3431
|
+
};
|
|
3432
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
3433
|
+
} catch (e: any) {
|
|
3434
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
3435
|
+
error: e?.message || String(e),
|
|
3436
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
3437
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
3438
|
+
});
|
|
3439
|
+
return {
|
|
3440
|
+
success: false,
|
|
3441
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
3442
|
+
validationSummary,
|
|
3443
|
+
patchEquivalence,
|
|
3444
|
+
refineStages,
|
|
3445
|
+
finalBranchConvergenceState: {
|
|
3446
|
+
branch,
|
|
3447
|
+
baseBranch,
|
|
3448
|
+
merged: false,
|
|
3449
|
+
removed: false,
|
|
3450
|
+
validation: 'passed',
|
|
3451
|
+
patchEquivalence: 'passed',
|
|
3452
|
+
status: 'not_mergeable',
|
|
3453
|
+
},
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
const submoduleAlignmentStarted = Date.now();
|
|
3458
|
+
const submoduleAlignment = await alignRefinerySubmodulesAfterMerge(repoRoot, baseHead, 'HEAD', {
|
|
3459
|
+
submoduleIgnorePaths: Array.isArray(sourceNode?.policy?.submoduleIgnorePaths)
|
|
3460
|
+
? sourceNode.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
3461
|
+
: undefined,
|
|
3462
|
+
});
|
|
3463
|
+
if (submoduleAlignment.status !== 'skipped') {
|
|
3464
|
+
recordMeshRefineStage(refineStages, 'submodule_alignment', submoduleAlignment.status, submoduleAlignmentStarted, {
|
|
3465
|
+
changedGitlinkPaths: submoduleAlignment.changedGitlinkPaths,
|
|
3466
|
+
outOfSyncPaths: submoduleAlignment.outOfSyncPaths,
|
|
3467
|
+
updatedPaths: submoduleAlignment.updatedPaths,
|
|
3468
|
+
verifiedPaths: submoduleAlignment.verifiedPaths,
|
|
3469
|
+
command: submoduleAlignment.command,
|
|
3470
|
+
error: submoduleAlignment.error,
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3473
|
+
if (submoduleAlignment.status === 'failed') {
|
|
3474
|
+
return {
|
|
3475
|
+
success: false,
|
|
3476
|
+
code: 'post_merge_submodule_alignment_failed',
|
|
3477
|
+
error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
|
|
3478
|
+
merged: true,
|
|
3479
|
+
branch,
|
|
3480
|
+
into: baseBranch,
|
|
3481
|
+
validationSummary,
|
|
3482
|
+
patchEquivalence,
|
|
3483
|
+
submoduleReachability,
|
|
3484
|
+
submoduleAlignment,
|
|
3485
|
+
mergeResult,
|
|
3486
|
+
refineStages,
|
|
3487
|
+
finalBranchConvergenceState: {
|
|
3488
|
+
branch: baseBranch,
|
|
3489
|
+
mergedBranch: branch,
|
|
3490
|
+
baseBranch,
|
|
3491
|
+
merged: true,
|
|
3492
|
+
removed: false,
|
|
3493
|
+
validation: 'passed',
|
|
3494
|
+
patchEquivalence: 'passed',
|
|
3495
|
+
submoduleReachability: 'passed',
|
|
3496
|
+
submoduleAlignment: 'failed',
|
|
3497
|
+
status: 'post_merge_alignment_failed',
|
|
3498
|
+
nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
|
|
3499
|
+
},
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
const cleanupStarted = Date.now();
|
|
3504
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
3505
|
+
meshId,
|
|
3506
|
+
nodeId,
|
|
3507
|
+
sessionCleanupMode: 'preserve',
|
|
3508
|
+
inlineMesh: args?.inlineMesh,
|
|
3509
|
+
});
|
|
3510
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
3511
|
+
removed: removeResult?.removed,
|
|
3512
|
+
code: removeResult?.code,
|
|
3513
|
+
error: removeResult?.error,
|
|
3514
|
+
});
|
|
3515
|
+
|
|
3516
|
+
let ledgerError: string | undefined;
|
|
3517
|
+
const ledgerStarted = Date.now();
|
|
3518
|
+
try {
|
|
3519
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3520
|
+
appendLedgerEntry(meshId, {
|
|
3521
|
+
kind: 'node_removed',
|
|
3522
|
+
nodeId,
|
|
3523
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment },
|
|
3524
|
+
});
|
|
3525
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
3526
|
+
} catch (e: any) {
|
|
3527
|
+
ledgerError = e?.message || String(e);
|
|
3528
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
const finalBranchConvergenceState = {
|
|
3532
|
+
branch: baseBranch,
|
|
3533
|
+
mergedBranch: branch,
|
|
3534
|
+
baseBranch,
|
|
3535
|
+
merged: true,
|
|
3536
|
+
removed: removeResult?.success !== false,
|
|
3537
|
+
validation: 'passed',
|
|
3538
|
+
patchEquivalence: 'passed',
|
|
3539
|
+
submoduleAlignment: submoduleAlignment.status,
|
|
3540
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
3541
|
+
};
|
|
3542
|
+
|
|
3543
|
+
if (removeResult?.success === false) {
|
|
3544
|
+
return {
|
|
3545
|
+
success: false,
|
|
3546
|
+
code: 'cleanup_failed',
|
|
3547
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
3548
|
+
merged: true,
|
|
3549
|
+
branch,
|
|
3550
|
+
into: baseBranch,
|
|
3551
|
+
removeResult,
|
|
3552
|
+
validationSummary,
|
|
3553
|
+
patchEquivalence,
|
|
3554
|
+
submoduleReachability,
|
|
3555
|
+
submoduleAlignment,
|
|
3556
|
+
mergeResult,
|
|
3557
|
+
refineStages,
|
|
3558
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3559
|
+
finalBranchConvergenceState,
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
return {
|
|
3564
|
+
success: true,
|
|
3565
|
+
merged: true,
|
|
3566
|
+
branch,
|
|
3567
|
+
into: baseBranch,
|
|
3568
|
+
removeResult,
|
|
3569
|
+
validationSummary,
|
|
3570
|
+
patchEquivalence,
|
|
3571
|
+
submoduleReachability,
|
|
3572
|
+
submoduleAlignment,
|
|
3573
|
+
mergeResult,
|
|
3574
|
+
refineStages,
|
|
3575
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3576
|
+
finalBranchConvergenceState,
|
|
3577
|
+
};
|
|
3578
|
+
} catch (e: any) {
|
|
3579
|
+
return { success: false, error: e.message, refineStages };
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
3584
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
3585
|
+
let result: Record<string, unknown>;
|
|
3586
|
+
try {
|
|
3587
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
3588
|
+
} catch (e: any) {
|
|
3589
|
+
result = { success: false, error: e?.message || String(e) };
|
|
3590
|
+
}
|
|
3591
|
+
const completedAt = new Date().toISOString();
|
|
3592
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
3593
|
+
meshId: handle.meshId,
|
|
3594
|
+
nodeId: handle.targetNodeId,
|
|
3595
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
3596
|
+
startedAt: handle.startedAt,
|
|
3597
|
+
completedAt,
|
|
3598
|
+
jobId: handle.jobId,
|
|
3599
|
+
interactionId: handle.interactionId,
|
|
3600
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3601
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
3602
|
+
});
|
|
3603
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
3604
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
3605
|
+
this.runningRefineJobs.delete(key);
|
|
3606
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
3607
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
3608
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
3612
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
3613
|
+
const running = this.runningRefineJobs.get(key);
|
|
3614
|
+
if (running) return { ...running, duplicate: true };
|
|
3615
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
3616
|
+
|
|
3617
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3618
|
+
const mesh = meshRecord?.mesh;
|
|
3619
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3620
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
3621
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
3622
|
+
|
|
3623
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
|
|
3624
|
+
this.runningRefineJobs.set(key, handle);
|
|
3625
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
3626
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
3627
|
+
|
|
3628
|
+
setImmediate(() => {
|
|
3629
|
+
void this.finishMeshRefineJob(handle, args);
|
|
3630
|
+
});
|
|
3631
|
+
|
|
3632
|
+
return handle;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
1465
3635
|
// ─── Daemon-level command core ───────────────────
|
|
1466
3636
|
|
|
1467
3637
|
/**
|
|
@@ -1476,13 +3646,35 @@ export class DaemonCommandRouter {
|
|
|
1476
3646
|
}
|
|
1477
3647
|
|
|
1478
3648
|
case 'get_pending_mesh_events': {
|
|
1479
|
-
const
|
|
3649
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3650
|
+
// (B3) Respect coordinatorDaemonId when the caller declares it
|
|
3651
|
+
// so unicast events route to the right coordinator instead of
|
|
3652
|
+
// being silently consumed by the first drainer.
|
|
3653
|
+
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
3654
|
+
? args.coordinatorDaemonId.trim()
|
|
3655
|
+
: undefined;
|
|
3656
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
|
|
1480
3657
|
return { success: true, events };
|
|
1481
3658
|
}
|
|
1482
3659
|
|
|
3660
|
+
case 'interactive_prompt_response': {
|
|
3661
|
+
const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
|
|
3662
|
+
? args.targetSessionId.trim()
|
|
3663
|
+
: typeof args?.sessionId === 'string' && args.sessionId.trim()
|
|
3664
|
+
? args.sessionId.trim()
|
|
3665
|
+
: '';
|
|
3666
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
3667
|
+
const response = normalizeInteractivePromptResponse(args?.response ?? args);
|
|
3668
|
+
const instance = this.deps.instanceManager.getInstance(sessionId);
|
|
3669
|
+
if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
|
|
3670
|
+
this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
|
|
3671
|
+
return { success: true };
|
|
3672
|
+
}
|
|
3673
|
+
|
|
1483
3674
|
case 'launch_cli':
|
|
1484
3675
|
case 'stop_cli':
|
|
1485
3676
|
case 'set_cli_view_mode':
|
|
3677
|
+
case 'record_provider_pty':
|
|
1486
3678
|
case 'agent_command': {
|
|
1487
3679
|
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
1488
3680
|
}
|
|
@@ -1678,7 +3870,7 @@ export class DaemonCommandRouter {
|
|
|
1678
3870
|
: '';
|
|
1679
3871
|
const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
|
|
1680
3872
|
const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
|
|
1681
|
-
canonicalHistory: providerMeta?.
|
|
3873
|
+
canonicalHistory: providerMeta?.nativeHistory,
|
|
1682
3874
|
offset,
|
|
1683
3875
|
limit,
|
|
1684
3876
|
historyBehavior: providerMeta?.historyBehavior,
|
|
@@ -1900,6 +4092,128 @@ export class DaemonCommandRouter {
|
|
|
1900
4092
|
};
|
|
1901
4093
|
}
|
|
1902
4094
|
|
|
4095
|
+
// Session-info popup data. Aggregates whatever the daemon knows
|
|
4096
|
+
// about a single live session into one envelope so the dashboard
|
|
4097
|
+
// doesn't need to stitch together status + coordinator registry +
|
|
4098
|
+
// session registry on the client. Includes the actual system
|
|
4099
|
+
// prompt that was injected at launch when the session is a mesh
|
|
4100
|
+
// coordinator — that's the "what prompt did the agent see?"
|
|
4101
|
+
// question the info-icon dialog is meant to answer.
|
|
4102
|
+
case 'get_session_info': {
|
|
4103
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
4104
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
4105
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
4106
|
+
// Fetch both lookups up front. We used to bail with "Session not
|
|
4107
|
+
// found" when sessionRegistry forgot the SID (auto-cleanup,
|
|
4108
|
+
// daemon restart with the session not yet restored, etc), which
|
|
4109
|
+
// hid the coordinator-side metadata even though the
|
|
4110
|
+
// coordinator-registry still has it. Now we return whichever
|
|
4111
|
+
// side we have. The dashboard renders "no coordinator-specific
|
|
4112
|
+
// prompt" only when *neither* side knows the session.
|
|
4113
|
+
const target = this.deps.sessionRegistry.get(sessionId);
|
|
4114
|
+
const coord = getCoordinatorForSession(sessionId);
|
|
4115
|
+
if (!target && !coord) return { success: false, error: 'Session not found', sessionId };
|
|
4116
|
+
const adapter = target
|
|
4117
|
+
? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter
|
|
4118
|
+
: undefined;
|
|
4119
|
+
const runtimeMeta = (adapter && typeof (adapter as any).getRuntimeMetadata === 'function')
|
|
4120
|
+
? (adapter as any).getRuntimeMetadata()
|
|
4121
|
+
: undefined;
|
|
4122
|
+
const providerType = target?.providerType || coord?.cliType || '';
|
|
4123
|
+
const providerMetaForSession = providerType
|
|
4124
|
+
? this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType)
|
|
4125
|
+
: undefined;
|
|
4126
|
+
return {
|
|
4127
|
+
success: true,
|
|
4128
|
+
session: {
|
|
4129
|
+
sessionId,
|
|
4130
|
+
providerType,
|
|
4131
|
+
providerName: providerMetaForSession?.name,
|
|
4132
|
+
transport: target?.transport,
|
|
4133
|
+
workspace: (target as any)?.workspace || coord?.workspace,
|
|
4134
|
+
spawnedAtMs: (target as any)?.spawnedAtMs || coord?.startedAt,
|
|
4135
|
+
providerSessionId: (target as any)?.providerSessionId,
|
|
4136
|
+
runtimeMetadata: runtimeMeta,
|
|
4137
|
+
},
|
|
4138
|
+
coordinator: coord ? {
|
|
4139
|
+
meshId: coord.meshId,
|
|
4140
|
+
startedAt: coord.startedAt,
|
|
4141
|
+
cliType: coord.cliType,
|
|
4142
|
+
systemPrompt: coord.systemPrompt,
|
|
4143
|
+
extraSystemPrompt: coord.extraSystemPrompt,
|
|
4144
|
+
injection: coord.injection,
|
|
4145
|
+
mcpConfigPath: coord.mcpConfigPath,
|
|
4146
|
+
} : null,
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
// ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
|
|
4151
|
+
// These live on this daemon's filesystem and never sync to the
|
|
4152
|
+
// cloud / other daemons — they're per-machine config. The
|
|
4153
|
+
// Settings page in the dashboard reads/writes via these two
|
|
4154
|
+
// commands instead of going through fs from the browser.
|
|
4155
|
+
case 'list_coordinator_prompts': {
|
|
4156
|
+
const fs = await import('node:fs');
|
|
4157
|
+
const path = await import('node:path');
|
|
4158
|
+
const os = await import('node:os');
|
|
4159
|
+
const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
|
|
4160
|
+
const entries: Record<string, { override: string; append: string }> = {};
|
|
4161
|
+
try {
|
|
4162
|
+
if (fs.existsSync(dir)) {
|
|
4163
|
+
for (const name of fs.readdirSync(dir)) {
|
|
4164
|
+
// Bucket files into <key>.{md|append.md}; ignore others
|
|
4165
|
+
// so a stray README or .DS_Store doesn't show up.
|
|
4166
|
+
const matchOverride = name.match(/^([a-zA-Z0-9_.-]+)\.md$/);
|
|
4167
|
+
const matchAppend = name.match(/^([a-zA-Z0-9_.-]+)\.append\.md$/);
|
|
4168
|
+
// append-pattern wins when both match (file is `.append.md`).
|
|
4169
|
+
const m = matchAppend || matchOverride;
|
|
4170
|
+
if (!m) continue;
|
|
4171
|
+
const isAppend = !!matchAppend;
|
|
4172
|
+
const key = m[1];
|
|
4173
|
+
const full = path.join(dir, name);
|
|
4174
|
+
let content = '';
|
|
4175
|
+
try { content = fs.readFileSync(full, 'utf8'); } catch { /* skip */ }
|
|
4176
|
+
if (!entries[key]) entries[key] = { override: '', append: '' };
|
|
4177
|
+
if (isAppend) entries[key].append = content;
|
|
4178
|
+
else entries[key].override = content;
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
} catch (error: any) {
|
|
4182
|
+
return { success: false, error: error?.message || String(error) };
|
|
4183
|
+
}
|
|
4184
|
+
return { success: true, dir, entries };
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
case 'write_coordinator_prompt': {
|
|
4188
|
+
const fs = await import('node:fs');
|
|
4189
|
+
const path = await import('node:path');
|
|
4190
|
+
const os = await import('node:os');
|
|
4191
|
+
const key = typeof args?.key === 'string' ? args.key.trim() : '';
|
|
4192
|
+
const kind = args?.kind === 'append' ? 'append' : 'override';
|
|
4193
|
+
const content = typeof args?.content === 'string' ? args.content : '';
|
|
4194
|
+
// Whitelist key chars so a malicious caller can't write
|
|
4195
|
+
// ../../etc/passwd. Same charset readUserPromptFile accepts.
|
|
4196
|
+
if (!key || !/^[a-zA-Z0-9_.-]+$/.test(key)) {
|
|
4197
|
+
return { success: false, error: 'key must match [a-zA-Z0-9_.-]+' };
|
|
4198
|
+
}
|
|
4199
|
+
const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
|
|
4200
|
+
const filename = kind === 'append' ? `${key}.append.md` : `${key}.md`;
|
|
4201
|
+
const full = path.join(dir, filename);
|
|
4202
|
+
try {
|
|
4203
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4204
|
+
if (content.trim()) {
|
|
4205
|
+
fs.writeFileSync(full, content, { encoding: 'utf8', mode: 0o600 });
|
|
4206
|
+
} else if (fs.existsSync(full)) {
|
|
4207
|
+
// Empty content = "reset to default" — delete the file
|
|
4208
|
+
// so the daemon's readUserPromptFile path falls through.
|
|
4209
|
+
fs.unlinkSync(full);
|
|
4210
|
+
}
|
|
4211
|
+
return { success: true, path: full, kind, key };
|
|
4212
|
+
} catch (error: any) {
|
|
4213
|
+
return { success: false, error: error?.message || String(error) };
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
|
|
1903
4217
|
case 'mark_session_seen': {
|
|
1904
4218
|
const sessionId = args?.sessionId;
|
|
1905
4219
|
if (!sessionId || typeof sessionId !== 'string') {
|
|
@@ -2074,50 +4388,309 @@ export class DaemonCommandRouter {
|
|
|
2074
4388
|
const { listMeshes } = await import('../config/mesh-config.js');
|
|
2075
4389
|
return { success: true, meshes: listMeshes() };
|
|
2076
4390
|
} catch (e: any) {
|
|
2077
|
-
return { success: false, error: e.message };
|
|
4391
|
+
return { success: false, error: e.message };
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
case 'get_mesh': {
|
|
4396
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4397
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4398
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4399
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
4400
|
+
|
|
4401
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
4402
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
4403
|
+
mesh: meshRecord.mesh,
|
|
4404
|
+
meshSource: meshRecord.source,
|
|
4405
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4406
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
4407
|
+
localMachineId: loadConfig().machineId || '',
|
|
4408
|
+
});
|
|
4409
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
4410
|
+
const sourceOfTruth = {
|
|
4411
|
+
membership: meshRecord.source === 'inline_cache'
|
|
4412
|
+
? 'coordinator_inline_mesh_cache'
|
|
4413
|
+
: meshRecord.source === 'local_config'
|
|
4414
|
+
? 'local_mesh_config'
|
|
4415
|
+
: 'inline_bootstrap_snapshot',
|
|
4416
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
4417
|
+
directPeerTruth: {
|
|
4418
|
+
required: requireDirectPeerTruth,
|
|
4419
|
+
satisfied: directTruthSatisfied,
|
|
4420
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4421
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4422
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4423
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4424
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4425
|
+
},
|
|
4426
|
+
};
|
|
4427
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4428
|
+
return {
|
|
4429
|
+
success: false,
|
|
4430
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
4431
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
4432
|
+
sourceOfTruth,
|
|
4433
|
+
};
|
|
4434
|
+
}
|
|
4435
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
case 'create_mesh': {
|
|
4439
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
4440
|
+
const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
|
|
4441
|
+
const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
|
|
4442
|
+
const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
|
|
4443
|
+
if (!name) return { success: false, error: 'name required' };
|
|
4444
|
+
try {
|
|
4445
|
+
const { createMesh } = await import('../config/mesh-config.js');
|
|
4446
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
4447
|
+
? args.meshHost
|
|
4448
|
+
: undefined;
|
|
4449
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
4450
|
+
return { success: true, mesh };
|
|
4451
|
+
} catch (e: any) {
|
|
4452
|
+
return { success: false, error: e.message };
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
case 'update_mesh': {
|
|
4457
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4458
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4459
|
+
try {
|
|
4460
|
+
const { updateMesh } = await import('../config/mesh-config.js');
|
|
4461
|
+
const patch: Record<string, unknown> = {};
|
|
4462
|
+
if (typeof args?.name === 'string') patch.name = args.name;
|
|
4463
|
+
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
4464
|
+
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
4465
|
+
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
4466
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
4467
|
+
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
4468
|
+
const mesh = updateMesh(meshId, patch as any);
|
|
4469
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4470
|
+
this.inlineMeshCache.set(meshId, mesh);
|
|
4471
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4472
|
+
return { success: true, mesh };
|
|
4473
|
+
} catch (e: any) {
|
|
4474
|
+
return { success: false, error: e.message };
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
case 'get_mesh_host_pairing': {
|
|
4479
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4480
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4481
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4482
|
+
const mesh = meshRecord?.mesh;
|
|
4483
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4484
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4485
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
4486
|
+
return {
|
|
4487
|
+
success: true,
|
|
4488
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
4489
|
+
meshId,
|
|
4490
|
+
hostAddress: meshHost.hostAddress,
|
|
4491
|
+
meshHost,
|
|
4492
|
+
manualPairing: {
|
|
4493
|
+
status: pairingStatus,
|
|
4494
|
+
joinImplemented: true,
|
|
4495
|
+
protocol: 'standalone_command_direct_v1',
|
|
4496
|
+
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.',
|
|
4497
|
+
},
|
|
4498
|
+
};
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
case 'configure_mesh_host_pairing': {
|
|
4502
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4503
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
4504
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4505
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4506
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
4507
|
+
try {
|
|
4508
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
4509
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
4510
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
4511
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
4512
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
4513
|
+
return {
|
|
4514
|
+
success: true,
|
|
4515
|
+
code: 'mesh_host_pairing_pending',
|
|
4516
|
+
meshId,
|
|
4517
|
+
hostAddress: configured.hostAddress,
|
|
4518
|
+
meshHost,
|
|
4519
|
+
manualPairing: {
|
|
4520
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
4521
|
+
joinImplemented: true,
|
|
4522
|
+
protocol: 'standalone_command_direct_v1',
|
|
4523
|
+
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.',
|
|
4524
|
+
},
|
|
4525
|
+
};
|
|
4526
|
+
} catch (e: any) {
|
|
4527
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
case 'create_mesh_host_pairing_token': {
|
|
4532
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4533
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4534
|
+
try {
|
|
4535
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
4536
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
4537
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
4538
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
4539
|
+
});
|
|
4540
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
4541
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
4542
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4543
|
+
return {
|
|
4544
|
+
success: true,
|
|
4545
|
+
code: 'mesh_host_pairing_token_created',
|
|
4546
|
+
meshId,
|
|
4547
|
+
token: created.token,
|
|
4548
|
+
tokenId: created.tokenId,
|
|
4549
|
+
expiresAt: created.expiresAt,
|
|
4550
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
4551
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
4552
|
+
};
|
|
4553
|
+
} catch (e: any) {
|
|
4554
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
2078
4555
|
}
|
|
2079
4556
|
}
|
|
2080
4557
|
|
|
2081
|
-
case '
|
|
4558
|
+
case 'apply_mesh_host_join': {
|
|
2082
4559
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4560
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4561
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
4562
|
+
? args.memberNode
|
|
4563
|
+
: null;
|
|
2083
4564
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2084
|
-
|
|
2085
|
-
if (meshRecord?.mesh) return { success: true, mesh: meshRecord.mesh };
|
|
2086
|
-
return { success: false, error: 'Mesh not found' };
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
case 'create_mesh': {
|
|
2090
|
-
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
2091
|
-
const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
|
|
2092
|
-
const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
|
|
2093
|
-
const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
|
|
2094
|
-
if (!name) return { success: false, error: 'name required' };
|
|
4565
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
2095
4566
|
try {
|
|
2096
|
-
const {
|
|
2097
|
-
const
|
|
2098
|
-
|
|
4567
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
4568
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
4569
|
+
token,
|
|
4570
|
+
memberNode: memberNode as any,
|
|
4571
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
4572
|
+
});
|
|
4573
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
4574
|
+
if (!applied.accepted) {
|
|
4575
|
+
return {
|
|
4576
|
+
success: false,
|
|
4577
|
+
code: 'mesh_host_join_rejected',
|
|
4578
|
+
meshId,
|
|
4579
|
+
tokenId: applied.tokenId,
|
|
4580
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
4581
|
+
error: applied.reason,
|
|
4582
|
+
};
|
|
4583
|
+
}
|
|
4584
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
4585
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4586
|
+
try {
|
|
4587
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
4588
|
+
appendLedgerEntry(meshId, {
|
|
4589
|
+
kind: 'node_joined',
|
|
4590
|
+
nodeId: applied.node.id,
|
|
4591
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
4592
|
+
});
|
|
4593
|
+
} catch { /* ledger append is best-effort */ }
|
|
4594
|
+
return {
|
|
4595
|
+
success: true,
|
|
4596
|
+
code: 'mesh_host_join_accepted',
|
|
4597
|
+
meshId,
|
|
4598
|
+
node: applied.node,
|
|
4599
|
+
tokenId: applied.tokenId,
|
|
4600
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
4601
|
+
};
|
|
2099
4602
|
} catch (e: any) {
|
|
2100
|
-
return { success: false, error: e.message };
|
|
4603
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
2101
4604
|
}
|
|
2102
4605
|
}
|
|
2103
4606
|
|
|
2104
|
-
case '
|
|
4607
|
+
case 'join_mesh_host_pairing': {
|
|
2105
4608
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4609
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
2106
4610
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4611
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
4612
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4613
|
+
const mesh = meshRecord?.mesh;
|
|
4614
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4615
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4616
|
+
if (meshHost.role !== 'member') {
|
|
4617
|
+
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.' };
|
|
4618
|
+
}
|
|
2107
4619
|
try {
|
|
2108
|
-
const {
|
|
2109
|
-
const
|
|
2110
|
-
if (
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
if (!
|
|
2115
|
-
const
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
4620
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
4621
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
4622
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
4623
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
4624
|
+
}
|
|
4625
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
4626
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
4627
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
4628
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
4629
|
+
? args.hostDaemonId.trim()
|
|
4630
|
+
: meshHost.hostDaemonId;
|
|
4631
|
+
let hostResult: any;
|
|
4632
|
+
let transport: string;
|
|
4633
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
4634
|
+
transport = 'mesh_command_dispatch';
|
|
4635
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
4636
|
+
meshId: hostMeshId,
|
|
4637
|
+
token,
|
|
4638
|
+
memberMeshId: meshId,
|
|
4639
|
+
memberNode,
|
|
4640
|
+
});
|
|
4641
|
+
} else if (meshHost.hostAddress) {
|
|
4642
|
+
transport = 'standalone_http_command';
|
|
4643
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
4644
|
+
const response = await fetch(commandUrl, {
|
|
4645
|
+
method: 'POST',
|
|
4646
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4647
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
4648
|
+
});
|
|
4649
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
4650
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
4651
|
+
} else {
|
|
4652
|
+
return {
|
|
4653
|
+
success: false,
|
|
4654
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
4655
|
+
meshId,
|
|
4656
|
+
meshHost,
|
|
4657
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
4658
|
+
};
|
|
4659
|
+
}
|
|
4660
|
+
if (!hostResult?.success) {
|
|
4661
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
4662
|
+
}
|
|
4663
|
+
const joined = meshRecord.inline
|
|
4664
|
+
? null
|
|
4665
|
+
: markMeshHostPairingJoined(meshId, {
|
|
4666
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
4667
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
4668
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
4669
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
4670
|
+
});
|
|
4671
|
+
if (joined) {
|
|
4672
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
4673
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4674
|
+
}
|
|
4675
|
+
return {
|
|
4676
|
+
success: true,
|
|
4677
|
+
code: 'mesh_host_join_applied',
|
|
4678
|
+
meshId,
|
|
4679
|
+
hostMeshId,
|
|
4680
|
+
transport,
|
|
4681
|
+
node: hostResult.node,
|
|
4682
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
4683
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
4684
|
+
hostResult,
|
|
4685
|
+
manualPairing: {
|
|
4686
|
+
status: 'paired',
|
|
4687
|
+
joinImplemented: true,
|
|
4688
|
+
protocol: 'standalone_command_direct_v1',
|
|
4689
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
4690
|
+
},
|
|
4691
|
+
};
|
|
2119
4692
|
} catch (e: any) {
|
|
2120
|
-
return { success: false, error: e.message };
|
|
4693
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
2121
4694
|
}
|
|
2122
4695
|
}
|
|
2123
4696
|
|
|
@@ -2214,6 +4787,8 @@ export class DaemonCommandRouter {
|
|
|
2214
4787
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2215
4788
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2216
4789
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4790
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
4791
|
+
if (ownerFailure) return ownerFailure;
|
|
2217
4792
|
try {
|
|
2218
4793
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2219
4794
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -2229,6 +4804,8 @@ export class DaemonCommandRouter {
|
|
|
2229
4804
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2230
4805
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2231
4806
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4807
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
4808
|
+
if (ownerFailure) return ownerFailure;
|
|
2232
4809
|
try {
|
|
2233
4810
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2234
4811
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -2250,6 +4827,8 @@ export class DaemonCommandRouter {
|
|
|
2250
4827
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2251
4828
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2252
4829
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
4830
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
4831
|
+
if (ownerFailure) return ownerFailure;
|
|
2253
4832
|
try {
|
|
2254
4833
|
const { addNode } = await import('../config/mesh-config.js');
|
|
2255
4834
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -2260,7 +4839,18 @@ export class DaemonCommandRouter {
|
|
|
2260
4839
|
...(readOnly ? { readOnly: true } : {}),
|
|
2261
4840
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
2262
4841
|
};
|
|
2263
|
-
const
|
|
4842
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
4843
|
+
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
4844
|
+
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
4845
|
+
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
4846
|
+
const node = addNode(meshId, {
|
|
4847
|
+
workspace,
|
|
4848
|
+
...(repoRoot ? { repoRoot } : {}),
|
|
4849
|
+
...(daemonId ? { daemonId } : {}),
|
|
4850
|
+
...(machineId ? { machineId } : {}),
|
|
4851
|
+
...(policy ? { policy } : {}),
|
|
4852
|
+
...(role ? { role } : {}),
|
|
4853
|
+
});
|
|
2264
4854
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
2265
4855
|
return { success: true, node };
|
|
2266
4856
|
} catch (e: any) {
|
|
@@ -2272,6 +4862,8 @@ export class DaemonCommandRouter {
|
|
|
2272
4862
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2273
4863
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2274
4864
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4865
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
4866
|
+
if (ownerFailure) return ownerFailure;
|
|
2275
4867
|
try {
|
|
2276
4868
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2277
4869
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2288,7 +4880,14 @@ export class DaemonCommandRouter {
|
|
|
2288
4880
|
delete (policy as any).providerPriority;
|
|
2289
4881
|
}
|
|
2290
4882
|
}
|
|
2291
|
-
const
|
|
4883
|
+
const patch: Record<string, unknown> = { policy: policy as any };
|
|
4884
|
+
if (typeof args?.systemPrompt === 'string') {
|
|
4885
|
+
const trimmed = (args.systemPrompt as string).trim();
|
|
4886
|
+
patch.systemPrompt = trimmed || undefined;
|
|
4887
|
+
} else if (args?.systemPrompt === null) {
|
|
4888
|
+
patch.systemPrompt = undefined;
|
|
4889
|
+
}
|
|
4890
|
+
const node = updateNode(meshId, nodeId, patch as any);
|
|
2292
4891
|
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
2293
4892
|
return { success: true, node };
|
|
2294
4893
|
} catch (e: any) {
|
|
@@ -2300,6 +4899,8 @@ export class DaemonCommandRouter {
|
|
|
2300
4899
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2301
4900
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2302
4901
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4902
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
4903
|
+
if (ownerFailure) return ownerFailure;
|
|
2303
4904
|
try {
|
|
2304
4905
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2305
4906
|
const mesh = meshRecord?.mesh;
|
|
@@ -2325,131 +4926,97 @@ export class DaemonCommandRouter {
|
|
|
2325
4926
|
}
|
|
2326
4927
|
}
|
|
2327
4928
|
|
|
2328
|
-
case '
|
|
4929
|
+
case 'get_mesh_refine_config_schema': {
|
|
4930
|
+
return {
|
|
4931
|
+
success: true,
|
|
4932
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
4933
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
4934
|
+
worktreeBootstrap: {
|
|
4935
|
+
schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
4936
|
+
locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
4937
|
+
sourceOfTruth: 'repo worktree bootstrap config',
|
|
4938
|
+
runBehavior: 'When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node.',
|
|
4939
|
+
},
|
|
4940
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
4941
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
case 'validate_mesh_refine_config': {
|
|
4946
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4947
|
+
const mesh = args?.inlineMesh || {};
|
|
4948
|
+
const loaded = args?.config !== undefined
|
|
4949
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
4950
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
4951
|
+
const validation = loaded.config
|
|
4952
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
4953
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
4954
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
case 'suggest_mesh_refine_config': {
|
|
4958
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4959
|
+
const mesh = args?.inlineMesh || {};
|
|
4960
|
+
return {
|
|
4961
|
+
success: true,
|
|
4962
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
4963
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
4964
|
+
};
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
case 'plan_mesh_refine_node': {
|
|
2329
4968
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2330
4969
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2331
4970
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2332
|
-
|
|
4971
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
4972
|
+
const mesh = meshRecord?.mesh;
|
|
4973
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
4974
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
4975
|
+
return {
|
|
4976
|
+
success: true,
|
|
4977
|
+
dryRun: true,
|
|
4978
|
+
nodeId,
|
|
4979
|
+
workspace: node.workspace,
|
|
4980
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
4981
|
+
mergeWillRun: false,
|
|
4982
|
+
cleanupWillRun: false,
|
|
4983
|
+
};
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
case 'fast_forward_mesh_node': {
|
|
4987
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4988
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
4989
|
+
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
4990
|
+
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
4991
|
+
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
4992
|
+
: undefined;
|
|
4993
|
+
if (!workspace && meshId && nodeId) {
|
|
2333
4994
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2334
4995
|
const mesh = meshRecord?.mesh;
|
|
2335
4996
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
return { success: false, error: `Refinery requires a local worktree node` };
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
const sourceNode = node.clonedFromNodeId
|
|
2343
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2344
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2345
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2346
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2347
|
-
|
|
2348
|
-
const { execFile } = await import('node:child_process');
|
|
2349
|
-
const { promisify } = await import('node:util');
|
|
2350
|
-
const execFileAsync = promisify(execFile);
|
|
2351
|
-
|
|
2352
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2353
|
-
const branch = branchStdout.trim();
|
|
2354
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2355
|
-
|
|
2356
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2357
|
-
const baseBranch = baseBranchStdout.trim();
|
|
2358
|
-
|
|
2359
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2360
|
-
if (validationSummary.status === 'failed') {
|
|
2361
|
-
return {
|
|
2362
|
-
success: false,
|
|
2363
|
-
code: 'validation_failed',
|
|
2364
|
-
convergenceStatus: 'blocked_review',
|
|
2365
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2366
|
-
branch,
|
|
2367
|
-
into: baseBranch,
|
|
2368
|
-
validationSummary,
|
|
2369
|
-
finalBranchConvergenceState: {
|
|
2370
|
-
branch,
|
|
2371
|
-
baseBranch,
|
|
2372
|
-
merged: false,
|
|
2373
|
-
removed: false,
|
|
2374
|
-
validation: 'failed',
|
|
2375
|
-
status: 'blocked_review',
|
|
2376
|
-
},
|
|
2377
|
-
};
|
|
2378
|
-
}
|
|
2379
|
-
if (validationSummary.status === 'skipped') {
|
|
2380
|
-
return {
|
|
2381
|
-
success: false,
|
|
2382
|
-
code: 'validation_unavailable',
|
|
2383
|
-
convergenceStatus: 'blocked_review',
|
|
2384
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2385
|
-
branch,
|
|
2386
|
-
into: baseBranch,
|
|
2387
|
-
validationSummary,
|
|
2388
|
-
finalBranchConvergenceState: {
|
|
2389
|
-
branch,
|
|
2390
|
-
baseBranch,
|
|
2391
|
-
merged: false,
|
|
2392
|
-
removed: false,
|
|
2393
|
-
validation: 'unavailable',
|
|
2394
|
-
status: 'blocked_review',
|
|
2395
|
-
},
|
|
2396
|
-
};
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
try {
|
|
2400
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2401
|
-
} catch (e: any) {
|
|
2402
|
-
return {
|
|
2403
|
-
success: false,
|
|
2404
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2405
|
-
validationSummary,
|
|
2406
|
-
finalBranchConvergenceState: {
|
|
2407
|
-
branch,
|
|
2408
|
-
baseBranch,
|
|
2409
|
-
merged: false,
|
|
2410
|
-
removed: false,
|
|
2411
|
-
validation: 'passed',
|
|
2412
|
-
status: 'not_mergeable',
|
|
2413
|
-
},
|
|
2414
|
-
};
|
|
4997
|
+
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
4998
|
+
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
4999
|
+
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
2415
5000
|
}
|
|
2416
|
-
|
|
2417
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
2418
|
-
meshId,
|
|
2419
|
-
nodeId,
|
|
2420
|
-
sessionCleanupMode: 'kill',
|
|
2421
|
-
inlineMesh: args?.inlineMesh,
|
|
2422
|
-
});
|
|
2423
|
-
|
|
2424
|
-
try {
|
|
2425
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2426
|
-
appendLedgerEntry(meshId, {
|
|
2427
|
-
kind: 'node_removed',
|
|
2428
|
-
nodeId,
|
|
2429
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
2430
|
-
});
|
|
2431
|
-
} catch {}
|
|
2432
|
-
|
|
2433
|
-
return {
|
|
2434
|
-
success: true,
|
|
2435
|
-
merged: true,
|
|
2436
|
-
branch,
|
|
2437
|
-
into: baseBranch,
|
|
2438
|
-
removeResult,
|
|
2439
|
-
validationSummary,
|
|
2440
|
-
finalBranchConvergenceState: {
|
|
2441
|
-
branch: baseBranch,
|
|
2442
|
-
mergedBranch: branch,
|
|
2443
|
-
baseBranch,
|
|
2444
|
-
merged: true,
|
|
2445
|
-
removed: removeResult?.success !== false,
|
|
2446
|
-
validation: 'passed',
|
|
2447
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2448
|
-
},
|
|
2449
|
-
};
|
|
2450
|
-
} catch (e: any) {
|
|
2451
|
-
return { success: false, error: e.message };
|
|
2452
5001
|
}
|
|
5002
|
+
const result = await (fastForwardMeshNode({
|
|
5003
|
+
meshId: meshId || undefined,
|
|
5004
|
+
nodeId: nodeId || undefined,
|
|
5005
|
+
workspace,
|
|
5006
|
+
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
5007
|
+
execute: args?.execute === true,
|
|
5008
|
+
dryRun: args?.dryRun === true,
|
|
5009
|
+
updateSubmodules: args?.updateSubmodules === true,
|
|
5010
|
+
submoduleIgnorePaths,
|
|
5011
|
+
}) as Promise<unknown>);
|
|
5012
|
+
return result as CommandRouterResult;
|
|
5013
|
+
}
|
|
5014
|
+
|
|
5015
|
+
case 'refine_mesh_node': {
|
|
5016
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
5017
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
5018
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
5019
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
2453
5020
|
}
|
|
2454
5021
|
|
|
2455
5022
|
case 'remove_mesh_node': {
|
|
@@ -2493,6 +5060,7 @@ export class DaemonCommandRouter {
|
|
|
2493
5060
|
} else {
|
|
2494
5061
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2495
5062
|
removed = removeNode(meshId, nodeId);
|
|
5063
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2496
5064
|
}
|
|
2497
5065
|
|
|
2498
5066
|
// Record in task ledger
|
|
@@ -2530,6 +5098,8 @@ export class DaemonCommandRouter {
|
|
|
2530
5098
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2531
5099
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2532
5100
|
if (!branch) return { success: false, error: 'branch required' };
|
|
5101
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
5102
|
+
if (ownerFailure) return ownerFailure;
|
|
2533
5103
|
|
|
2534
5104
|
try {
|
|
2535
5105
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2578,39 +5148,124 @@ export class DaemonCommandRouter {
|
|
|
2578
5148
|
policy: { ...(sourceNode.policy || {}) },
|
|
2579
5149
|
});
|
|
2580
5150
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
5151
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2581
5152
|
}
|
|
2582
5153
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
5154
|
+
const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
5155
|
+
node.worktreeBootstrap = bootstrapState;
|
|
5156
|
+
if (meshRecord.inline) {
|
|
5157
|
+
this.updateInlineMeshNode(meshId, mesh, node);
|
|
5158
|
+
return;
|
|
5159
|
+
}
|
|
2586
5160
|
try {
|
|
2587
|
-
const {
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
5161
|
+
const { updateNode } = await import('../config/mesh-config.js');
|
|
5162
|
+
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
5163
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
5164
|
+
} catch { /* bootstrap status persistence is best-effort */ }
|
|
5165
|
+
};
|
|
5166
|
+
|
|
5167
|
+
const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
5168
|
+
try {
|
|
5169
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
5170
|
+
appendLedgerEntry(meshId, {
|
|
5171
|
+
kind: 'node_cloned',
|
|
5172
|
+
nodeId: node.id,
|
|
5173
|
+
payload: {
|
|
5174
|
+
sourceNodeId,
|
|
5175
|
+
branch: result.branch,
|
|
5176
|
+
worktreePath: result.worktreePath,
|
|
5177
|
+
submodulesInitialized: initSubmodules,
|
|
5178
|
+
worktreeBootstrap: {
|
|
5179
|
+
status: bootstrapState.status,
|
|
5180
|
+
required: bootstrapState.required,
|
|
5181
|
+
configSource: bootstrapState.configSource,
|
|
5182
|
+
configSourceType: bootstrapState.configSourceType,
|
|
5183
|
+
lastCommand: bootstrapState.lastCommand,
|
|
5184
|
+
exitCode: bootstrapState.exitCode,
|
|
5185
|
+
},
|
|
5186
|
+
},
|
|
5187
|
+
});
|
|
5188
|
+
} catch { /* ledger append is best-effort */ }
|
|
5189
|
+
};
|
|
5190
|
+
|
|
5191
|
+
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
5192
|
+
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
|
|
5193
|
+
const runningBootstrapState: WorktreeBootstrapState = {
|
|
5194
|
+
status: 'running',
|
|
5195
|
+
required: loadedBootstrap.config?.required !== false,
|
|
5196
|
+
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
5197
|
+
configSourceType: loadedBootstrap.sourceType,
|
|
5198
|
+
startedAt: new Date().toISOString(),
|
|
5199
|
+
};
|
|
5200
|
+
await persistWorktreeSetupState(runningBootstrapState);
|
|
5201
|
+
|
|
5202
|
+
const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
|
|
5203
|
+
let submodulesInitialized = false;
|
|
5204
|
+
if (initSubmodules) {
|
|
5205
|
+
try {
|
|
5206
|
+
const { runGit } = await import('../git/git-executor.js');
|
|
5207
|
+
await runGit(
|
|
5208
|
+
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
5209
|
+
['submodule', 'update', '--init', '--recursive'],
|
|
5210
|
+
{ timeoutMs: 120000 },
|
|
5211
|
+
);
|
|
5212
|
+
submodulesInitialized = true;
|
|
5213
|
+
} catch (subErr: any) {
|
|
5214
|
+
// Submodule init is best-effort; don't fail the clone
|
|
5215
|
+
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
5216
|
+
}
|
|
2596
5217
|
}
|
|
2597
|
-
|
|
5218
|
+
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
5219
|
+
await persistWorktreeSetupState(bootstrapState);
|
|
5220
|
+
await appendCloneLedger(submodulesInitialized, bootstrapState);
|
|
5221
|
+
return { submodulesInitialized, bootstrapState };
|
|
5222
|
+
};
|
|
2598
5223
|
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
5224
|
+
const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
|
|
5225
|
+
const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
|
|
5226
|
+
? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
|
|
5227
|
+
: 8000;
|
|
5228
|
+
const setupPromise = finishWorktreeSetup();
|
|
5229
|
+
const setupResult = await Promise.race([
|
|
5230
|
+
setupPromise.then((value) => ({ completed: true as const, value })),
|
|
5231
|
+
new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
|
|
5232
|
+
]);
|
|
5233
|
+
|
|
5234
|
+
if (!setupResult.completed) {
|
|
5235
|
+
setupPromise.catch((error: any) => {
|
|
5236
|
+
const failedState: WorktreeBootstrapState = {
|
|
5237
|
+
...runningBootstrapState,
|
|
5238
|
+
status: 'failed',
|
|
5239
|
+
completedAt: new Date().toISOString(),
|
|
5240
|
+
error: error?.message || String(error),
|
|
5241
|
+
};
|
|
5242
|
+
void persistWorktreeSetupState(failedState);
|
|
5243
|
+
void appendCloneLedger(false, failedState);
|
|
2606
5244
|
});
|
|
2607
|
-
|
|
5245
|
+
return {
|
|
5246
|
+
success: true,
|
|
5247
|
+
async: true,
|
|
5248
|
+
status: 'accepted',
|
|
5249
|
+
node,
|
|
5250
|
+
worktreePath: result.worktreePath,
|
|
5251
|
+
branch: result.branch,
|
|
5252
|
+
worktreeBootstrap: runningBootstrapState,
|
|
5253
|
+
worktreeSetup: {
|
|
5254
|
+
status: 'running',
|
|
5255
|
+
setupWaitMs,
|
|
5256
|
+
message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
|
|
5257
|
+
},
|
|
5258
|
+
};
|
|
5259
|
+
}
|
|
2608
5260
|
|
|
5261
|
+
const { submodulesInitialized, bootstrapState } = setupResult.value;
|
|
2609
5262
|
return {
|
|
2610
5263
|
success: true,
|
|
2611
5264
|
node,
|
|
2612
5265
|
worktreePath: result.worktreePath,
|
|
2613
5266
|
branch: result.branch,
|
|
5267
|
+
submodulesInitialized,
|
|
5268
|
+
worktreeBootstrap: bootstrapState,
|
|
2614
5269
|
};
|
|
2615
5270
|
} catch (e: any) {
|
|
2616
5271
|
return { success: false, error: e.message };
|
|
@@ -2619,6 +5274,8 @@ export class DaemonCommandRouter {
|
|
|
2619
5274
|
case 'trigger_mesh_queue': {
|
|
2620
5275
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2621
5276
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
5277
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
5278
|
+
if (ownerFailure) return ownerFailure;
|
|
2622
5279
|
try {
|
|
2623
5280
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2624
5281
|
if (meshId) {
|
|
@@ -2634,6 +5291,19 @@ export class DaemonCommandRouter {
|
|
|
2634
5291
|
case 'launch_mesh_coordinator': {
|
|
2635
5292
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2636
5293
|
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
5294
|
+
// Optional per-launch system-prompt addition. Dashboard or API
|
|
5295
|
+
// callers (e.g. when spawning a mesh-node-specific coordinator)
|
|
5296
|
+
// can pass extra context that gets appended to the rendered
|
|
5297
|
+
// default prompt under the "## Additional Context" section.
|
|
5298
|
+
// Going through buildCoordinatorSystemPrompt's userInstruction
|
|
5299
|
+
// means user-level override files (~/.adhdev/coordinator-prompts)
|
|
5300
|
+
// and this per-launch addition compose cleanly: an override
|
|
5301
|
+
// wins outright, but if there's no override, the default
|
|
5302
|
+
// prompt + the optional append.md file + this extra context
|
|
5303
|
+
// all stack in declared order.
|
|
5304
|
+
const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
|
|
5305
|
+
? args.extraSystemPrompt.trim()
|
|
5306
|
+
: '';
|
|
2637
5307
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2638
5308
|
|
|
2639
5309
|
try {
|
|
@@ -2650,6 +5320,15 @@ export class DaemonCommandRouter {
|
|
|
2650
5320
|
mesh = getMesh(meshId);
|
|
2651
5321
|
}
|
|
2652
5322
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
5323
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
5324
|
+
if (!meshHost.canOwnCoordinator) {
|
|
5325
|
+
return {
|
|
5326
|
+
success: false,
|
|
5327
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
5328
|
+
meshId,
|
|
5329
|
+
cliType,
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
2653
5332
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2654
5333
|
|
|
2655
5334
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2669,7 +5348,16 @@ export class DaemonCommandRouter {
|
|
|
2669
5348
|
cliType,
|
|
2670
5349
|
};
|
|
2671
5350
|
}
|
|
2672
|
-
const
|
|
5351
|
+
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
5352
|
+
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
5353
|
+
: [];
|
|
5354
|
+
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
5355
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
5356
|
+
meshId,
|
|
5357
|
+
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || ''),
|
|
5358
|
+
liveSessionRecords: liveMeshSessions,
|
|
5359
|
+
allowCoordinatorSession: true,
|
|
5360
|
+
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
2673
5361
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2674
5362
|
if (!cliType) {
|
|
2675
5363
|
const resolved = await resolveProviderTypeFromPriority({
|
|
@@ -2726,7 +5414,7 @@ export class DaemonCommandRouter {
|
|
|
2726
5414
|
// Build coordinator prompt first — fail closed on errors.
|
|
2727
5415
|
let cliCmdSystemPrompt = '';
|
|
2728
5416
|
try {
|
|
2729
|
-
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
5417
|
+
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
|
|
2730
5418
|
} catch (error: any) {
|
|
2731
5419
|
const message = error?.message || String(error);
|
|
2732
5420
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -2738,53 +5426,45 @@ export class DaemonCommandRouter {
|
|
|
2738
5426
|
};
|
|
2739
5427
|
}
|
|
2740
5428
|
|
|
2741
|
-
// Run the provider's MCP registration command
|
|
5429
|
+
// Run the provider's MCP registration command under a
|
|
5430
|
+
// PTY. Some providers (agy, future bubbletea CLIs)
|
|
5431
|
+
// refuse to run without /dev/tty, so pipe-only
|
|
5432
|
+
// execFileSync silently no-ops the registration and
|
|
5433
|
+
// the coordinator ends up without any mcp tools. With
|
|
5434
|
+
// a real PTY the registration goes through and the
|
|
5435
|
+
// exit code tells us whether it actually persisted.
|
|
5436
|
+
let mcpRegistrationOk = false;
|
|
2742
5437
|
try {
|
|
2743
|
-
const {
|
|
5438
|
+
const { execUnderPty } = await import('./mesh-coordinator.js');
|
|
2744
5439
|
const cmdParts = coordinatorSetup.command.trim().split(/\s+/);
|
|
2745
5440
|
const [regCmd, ...regArgs] = cmdParts;
|
|
2746
|
-
LOG.info('MeshCoordinator', `Running MCP registration: ${coordinatorSetup.command}`);
|
|
2747
|
-
|
|
5441
|
+
LOG.info('MeshCoordinator', `Running MCP registration (pty): ${coordinatorSetup.command}`);
|
|
5442
|
+
const ptyResult = await execUnderPty(regCmd, regArgs, { cwd: workspace, timeoutMs: 20_000 });
|
|
5443
|
+
if (ptyResult.timedOut) {
|
|
5444
|
+
LOG.warn('MeshCoordinator', `MCP registration timed out — last output:\n${ptyResult.output.slice(-2000)}`);
|
|
5445
|
+
} else if (ptyResult.exitCode === 0) {
|
|
5446
|
+
mcpRegistrationOk = true;
|
|
5447
|
+
LOG.info('MeshCoordinator', `MCP registration succeeded (exit=0)`);
|
|
5448
|
+
} else {
|
|
5449
|
+
// Non-fatal — many providers return non-zero on duplicate registration.
|
|
5450
|
+
LOG.warn('MeshCoordinator', `MCP registration exit=${ptyResult.exitCode} signal=${ptyResult.signal} — output:\n${ptyResult.output.slice(-2000)}`);
|
|
5451
|
+
}
|
|
2748
5452
|
} catch (error: any) {
|
|
2749
|
-
|
|
2750
|
-
LOG.warn('MeshCoordinator', `MCP registration command failed (may be pre-registered): ${error?.message || error}`);
|
|
5453
|
+
LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
|
|
2751
5454
|
}
|
|
2752
5455
|
|
|
2753
|
-
// Inject system prompt
|
|
2754
|
-
// Codex: -c 'instructions="..."' CLI config override
|
|
2755
|
-
// Gemini: write GEMINI.md to workspace (auto-loaded as context)
|
|
5456
|
+
// Inject system prompt declaratively from provider.v1.json.
|
|
2756
5457
|
const cliCmdArgs: string[] = [];
|
|
2757
5458
|
const cliCmdEnv: Record<string, string> = {};
|
|
5459
|
+
let cliCmdContextFilePath: string | undefined;
|
|
2758
5460
|
if (cliCmdSystemPrompt) {
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
try {
|
|
2767
|
-
const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
|
|
2768
|
-
const geminiMdPath = `${workspace}/GEMINI.md`;
|
|
2769
|
-
const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
|
|
2770
|
-
const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
|
|
2771
|
-
const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
|
|
2772
|
-
if (efs(geminiMdPath)) {
|
|
2773
|
-
const existing = rfs(geminiMdPath, 'utf-8');
|
|
2774
|
-
// Replace existing block or append
|
|
2775
|
-
const replaced = existing.replace(
|
|
2776
|
-
new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
|
|
2777
|
-
block,
|
|
2778
|
-
);
|
|
2779
|
-
wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
|
|
2780
|
-
} else {
|
|
2781
|
-
wfs(geminiMdPath, block);
|
|
2782
|
-
}
|
|
2783
|
-
LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
|
|
2784
|
-
} catch (e: any) {
|
|
2785
|
-
LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
5461
|
+
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
5462
|
+
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
5463
|
+
cliCmdSystemPrompt,
|
|
5464
|
+
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
5465
|
+
{ cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
|
|
5466
|
+
);
|
|
5467
|
+
cliCmdContextFilePath = effect.contextFilePath;
|
|
2788
5468
|
}
|
|
2789
5469
|
|
|
2790
5470
|
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
@@ -2795,16 +5475,52 @@ export class DaemonCommandRouter {
|
|
|
2795
5475
|
settings: { meshCoordinatorFor: meshId },
|
|
2796
5476
|
});
|
|
2797
5477
|
|
|
5478
|
+
// R48 inject-then-remove. Spawn was just kicked off above; agy and
|
|
5479
|
+
// gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
|
|
5480
|
+
// cache it for the rest of the session, so we can safely strip
|
|
5481
|
+
// the wrapper from disk shortly after launch. That keeps any
|
|
5482
|
+
// worker session launched into the same workspace later from
|
|
5483
|
+
// picking up our wrapper block.
|
|
5484
|
+
if (cliCmdLaunch?.success && cliCmdContextFilePath) {
|
|
5485
|
+
const stripPath = cliCmdContextFilePath;
|
|
5486
|
+
setTimeout(() => {
|
|
5487
|
+
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
5488
|
+
stripCoordinatorWrapperFile(stripPath);
|
|
5489
|
+
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
|
|
5490
|
+
}).catch(() => { /* best-effort */ });
|
|
5491
|
+
}, 5000);
|
|
5492
|
+
}
|
|
5493
|
+
|
|
2798
5494
|
if (!cliCmdLaunch?.success) {
|
|
2799
5495
|
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
2800
5496
|
}
|
|
2801
5497
|
|
|
2802
5498
|
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
5499
|
+
const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
|
|
5500
|
+
if (cliCmdSessionId) {
|
|
5501
|
+
const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
5502
|
+
registerMeshCoordinator({
|
|
5503
|
+
meshId,
|
|
5504
|
+
sessionId: cliCmdSessionId,
|
|
5505
|
+
workspace,
|
|
5506
|
+
startedAt: Date.now(),
|
|
5507
|
+
cliType,
|
|
5508
|
+
systemPrompt: cliCmdSystemPrompt || undefined,
|
|
5509
|
+
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
5510
|
+
injection: cliCmdInjectionDecl ? {
|
|
5511
|
+
mode: cliCmdInjectionDecl.mode,
|
|
5512
|
+
target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
|
|
5513
|
+
: 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
|
|
5514
|
+
: 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
|
|
5515
|
+
: undefined,
|
|
5516
|
+
} : undefined,
|
|
5517
|
+
});
|
|
5518
|
+
}
|
|
2803
5519
|
try {
|
|
2804
5520
|
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2805
5521
|
appendLedgerEntry(meshId, {
|
|
2806
5522
|
kind: 'coordinator_started',
|
|
2807
|
-
sessionId:
|
|
5523
|
+
sessionId: cliCmdSessionId,
|
|
2808
5524
|
providerType: cliType,
|
|
2809
5525
|
payload: { workspace },
|
|
2810
5526
|
});
|
|
@@ -2815,8 +5531,8 @@ export class DaemonCommandRouter {
|
|
|
2815
5531
|
meshId,
|
|
2816
5532
|
cliType,
|
|
2817
5533
|
workspace,
|
|
2818
|
-
sessionId:
|
|
2819
|
-
mcpRegistered:
|
|
5534
|
+
sessionId: cliCmdSessionId,
|
|
5535
|
+
mcpRegistered: mcpRegistrationOk,
|
|
2820
5536
|
};
|
|
2821
5537
|
}
|
|
2822
5538
|
|
|
@@ -2837,7 +5553,7 @@ export class DaemonCommandRouter {
|
|
|
2837
5553
|
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
2838
5554
|
let systemPrompt = '';
|
|
2839
5555
|
try {
|
|
2840
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
5556
|
+
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
|
|
2841
5557
|
} catch (error: any) {
|
|
2842
5558
|
const message = error?.message || String(error);
|
|
2843
5559
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -2952,12 +5668,15 @@ export class DaemonCommandRouter {
|
|
|
2952
5668
|
launchEnv.HERMES_HOME = dirname(mcpConfigPath);
|
|
2953
5669
|
launchEnv.HERMES_IGNORE_USER_CONFIG = '';
|
|
2954
5670
|
}
|
|
5671
|
+
let autoImportContextFilePath: string | undefined;
|
|
2955
5672
|
if (systemPrompt) {
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
5673
|
+
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
5674
|
+
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
5675
|
+
systemPrompt,
|
|
5676
|
+
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
5677
|
+
{ cliArgs, launchEnv, workspace, cliType },
|
|
5678
|
+
);
|
|
5679
|
+
autoImportContextFilePath = effect.contextFilePath;
|
|
2961
5680
|
}
|
|
2962
5681
|
if (cliType === 'claude-cli') {
|
|
2963
5682
|
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
@@ -2976,18 +5695,53 @@ export class DaemonCommandRouter {
|
|
|
2976
5695
|
}
|
|
2977
5696
|
});
|
|
2978
5697
|
|
|
5698
|
+
// R48 inject-then-remove. See the cli_command branch for context;
|
|
5699
|
+
// same idea: strip the wrapper from disk ~5s after launch so the
|
|
5700
|
+
// user's AGENTS.md / GEMINI.md is untouched the moment any
|
|
5701
|
+
// worker session opens up in the same workspace.
|
|
5702
|
+
if (launchResult?.success && autoImportContextFilePath) {
|
|
5703
|
+
const stripPath = autoImportContextFilePath;
|
|
5704
|
+
setTimeout(() => {
|
|
5705
|
+
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
5706
|
+
stripCoordinatorWrapperFile(stripPath);
|
|
5707
|
+
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
|
|
5708
|
+
}).catch(() => { /* best-effort */ });
|
|
5709
|
+
}, 5000);
|
|
5710
|
+
}
|
|
5711
|
+
|
|
2979
5712
|
if (!launchResult?.success) {
|
|
2980
5713
|
return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
|
|
2981
5714
|
}
|
|
2982
5715
|
|
|
2983
5716
|
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
|
|
5717
|
+
const launchSessionId = launchResult.sessionId || launchResult.id;
|
|
5718
|
+
if (launchSessionId) {
|
|
5719
|
+
const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
5720
|
+
registerMeshCoordinator({
|
|
5721
|
+
meshId,
|
|
5722
|
+
sessionId: launchSessionId,
|
|
5723
|
+
workspace,
|
|
5724
|
+
startedAt: Date.now(),
|
|
5725
|
+
cliType,
|
|
5726
|
+
systemPrompt: systemPrompt || undefined,
|
|
5727
|
+
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
5728
|
+
mcpConfigPath,
|
|
5729
|
+
injection: autoImportInjectionDecl ? {
|
|
5730
|
+
mode: autoImportInjectionDecl.mode,
|
|
5731
|
+
target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
|
|
5732
|
+
: 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
|
|
5733
|
+
: 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
|
|
5734
|
+
: undefined,
|
|
5735
|
+
} : undefined,
|
|
5736
|
+
});
|
|
5737
|
+
}
|
|
2984
5738
|
|
|
2985
5739
|
// Record coordinator launch in task ledger
|
|
2986
5740
|
try {
|
|
2987
5741
|
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2988
5742
|
appendLedgerEntry(meshId, {
|
|
2989
5743
|
kind: 'coordinator_started',
|
|
2990
|
-
sessionId:
|
|
5744
|
+
sessionId: launchSessionId,
|
|
2991
5745
|
providerType: cliType,
|
|
2992
5746
|
payload: { workspace },
|
|
2993
5747
|
});
|
|
@@ -2998,7 +5752,7 @@ export class DaemonCommandRouter {
|
|
|
2998
5752
|
meshId,
|
|
2999
5753
|
cliType,
|
|
3000
5754
|
workspace,
|
|
3001
|
-
sessionId:
|
|
5755
|
+
sessionId: launchSessionId,
|
|
3002
5756
|
mcpConfigWritten: true,
|
|
3003
5757
|
};
|
|
3004
5758
|
} catch (e: any) {
|
|
@@ -3014,6 +5768,36 @@ export class DaemonCommandRouter {
|
|
|
3014
5768
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3015
5769
|
const mesh = meshRecord?.mesh;
|
|
3016
5770
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
5771
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
5772
|
+
|
|
5773
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
5774
|
+
// See (B3) below: scope the peek to this daemon when the
|
|
5775
|
+
// caller doesn't tell us, otherwise scoped events look
|
|
5776
|
+
// missing and we falsely return a stale cache.
|
|
5777
|
+
const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
5778
|
+
? args.coordinatorDaemonId.trim()
|
|
5779
|
+
: (this.deps.statusInstanceId || undefined);
|
|
5780
|
+
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
|
|
5781
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
5782
|
+
if (!refreshRequested && pendingCoordinatorEventCount === 0) {
|
|
5783
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
5784
|
+
if (cachedStatus) {
|
|
5785
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
5786
|
+
meshId,
|
|
5787
|
+
command: 'mesh_status',
|
|
5788
|
+
refreshRequested,
|
|
5789
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
5790
|
+
});
|
|
5791
|
+
return cachedStatus;
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
const refreshReason = refreshRequested
|
|
5795
|
+
? 'explicit_refresh'
|
|
5796
|
+
: pendingCoordinatorEventCount > 0
|
|
5797
|
+
? 'pending_coordinator_events'
|
|
5798
|
+
: hadAggregateCache
|
|
5799
|
+
? 'stale_pending_cache_refresh'
|
|
5800
|
+
: 'cold_cache_miss';
|
|
3017
5801
|
|
|
3018
5802
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
3019
5803
|
const queue = getQueue(meshId);
|
|
@@ -3021,6 +5805,7 @@ export class DaemonCommandRouter {
|
|
|
3021
5805
|
|
|
3022
5806
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
3023
5807
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
5808
|
+
const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
|
|
3024
5809
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
3025
5810
|
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
3026
5811
|
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
@@ -3028,29 +5813,123 @@ export class DaemonCommandRouter {
|
|
|
3028
5813
|
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
3029
5814
|
|
|
3030
5815
|
const localMachineId = loadConfig().machineId || '';
|
|
5816
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
5817
|
+
const directTruth = requireDirectPeerTruth
|
|
5818
|
+
? await hydrateInlineMeshDirectTruth({
|
|
5819
|
+
mesh,
|
|
5820
|
+
meshSource: meshRecord.source,
|
|
5821
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5822
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
5823
|
+
localMachineId,
|
|
5824
|
+
})
|
|
5825
|
+
: {
|
|
5826
|
+
directEvidenceCount: 0,
|
|
5827
|
+
localConfirmedCount: 0,
|
|
5828
|
+
peerAttemptedCount: 0,
|
|
5829
|
+
peerConfirmedCount: 0,
|
|
5830
|
+
unavailableNodeIds: [] as string[],
|
|
5831
|
+
};
|
|
5832
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
5833
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
5834
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
5835
|
+
&& !refreshRequested
|
|
5836
|
+
&& directTruth.directEvidenceCount > 0
|
|
5837
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
5838
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
5839
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
5840
|
+
: directTruth;
|
|
5841
|
+
const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
5842
|
+
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
5843
|
+
&& Array.isArray(mesh.nodes)
|
|
5844
|
+
&& mesh.nodes
|
|
5845
|
+
.filter((node: any) => unavailableDirectTruthNodeIds.has(String(node.id || node.nodeId || '')))
|
|
5846
|
+
.every((node: any) => node?.isLocalWorktree === true);
|
|
5847
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
5848
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
5849
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
5850
|
+
const failureResult = {
|
|
5851
|
+
success: false,
|
|
5852
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
5853
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
5854
|
+
sourceOfTruth: {
|
|
5855
|
+
membership: meshRecord.source === 'inline_cache'
|
|
5856
|
+
? 'coordinator_inline_mesh_cache'
|
|
5857
|
+
: meshRecord.source === 'local_config'
|
|
5858
|
+
? 'local_mesh_config'
|
|
5859
|
+
: 'inline_bootstrap_snapshot',
|
|
5860
|
+
coordinatorOwnsLiveTruth: false,
|
|
5861
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
5862
|
+
directPeerTruth: {
|
|
5863
|
+
required: true,
|
|
5864
|
+
satisfied: false,
|
|
5865
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
5866
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
5867
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
5868
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
5869
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
5870
|
+
},
|
|
5871
|
+
},
|
|
5872
|
+
};
|
|
5873
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
5874
|
+
meshId,
|
|
5875
|
+
command: 'mesh_status',
|
|
5876
|
+
refreshRequested,
|
|
5877
|
+
meshSource: meshRecord.source,
|
|
5878
|
+
directTruth,
|
|
5879
|
+
});
|
|
5880
|
+
return failureResult;
|
|
5881
|
+
}
|
|
5882
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
5883
|
+
const coordinatorHostname = osHostname();
|
|
5884
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
5885
|
+
mesh.coordinator?.preferredNodeId,
|
|
5886
|
+
(mesh.nodes?.[0] as any)?.id,
|
|
5887
|
+
(mesh.nodes?.[0] as any)?.nodeId,
|
|
5888
|
+
);
|
|
3031
5889
|
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
3032
|
-
?
|
|
5890
|
+
? selectedCoordinatorNodeId
|
|
3033
5891
|
: undefined;
|
|
3034
5892
|
const refreshedAt = new Date().toISOString();
|
|
3035
5893
|
const nodeStatuses = [];
|
|
3036
5894
|
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
3037
5895
|
const nodeId = String(node.id || node.nodeId || '');
|
|
3038
5896
|
const daemonId = readStringValue(node.daemonId);
|
|
5897
|
+
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
|
|
5898
|
+
const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
|
|
3039
5899
|
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
5900
|
+
const configuredCoordinatorNode = Boolean(
|
|
5901
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
5902
|
+
);
|
|
5903
|
+
const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
|
|
5904
|
+
&& !daemonId
|
|
5905
|
+
&& !nodeMachineId
|
|
5906
|
+
&& !nodeHostname;
|
|
3040
5907
|
const isSelfNode = Boolean(
|
|
3041
5908
|
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
3042
5909
|
) || Boolean(
|
|
3043
5910
|
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
3044
|
-
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
5911
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
5912
|
+
|| sparseConfiguredCoordinatorNode;
|
|
5913
|
+
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
5914
|
+
localMachineId,
|
|
5915
|
+
localDaemonId: this.deps.statusInstanceId,
|
|
5916
|
+
coordinatorHostname,
|
|
5917
|
+
isSelfNode,
|
|
5918
|
+
});
|
|
3045
5919
|
const status: Record<string, unknown> = {
|
|
3046
5920
|
nodeId,
|
|
3047
|
-
machineLabel: node
|
|
5921
|
+
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
5922
|
+
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
5923
|
+
? 'explicit_metadata'
|
|
5924
|
+
: 'workspace_host_provider_context',
|
|
3048
5925
|
workspace: node.workspace,
|
|
3049
5926
|
repoRoot: node.repoRoot,
|
|
3050
5927
|
isLocalWorktree: node.isLocalWorktree,
|
|
3051
5928
|
worktreeBranch: node.worktreeBranch,
|
|
5929
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
3052
5930
|
daemonId,
|
|
3053
|
-
machineId: node.machineId,
|
|
5931
|
+
machineId: nodeMachineId || node.machineId,
|
|
5932
|
+
machine: machineIdentity,
|
|
3054
5933
|
machineStatus: node.machineStatus,
|
|
3055
5934
|
health: 'unknown',
|
|
3056
5935
|
providers: node.providers || [],
|
|
@@ -3089,8 +5968,20 @@ export class DaemonCommandRouter {
|
|
|
3089
5968
|
reason: 'Node has no daemon id, so mesh transport cannot be reported from the selected coordinator.',
|
|
3090
5969
|
};
|
|
3091
5970
|
}
|
|
3092
|
-
const matchedLiveSessionRecords =
|
|
3093
|
-
|
|
5971
|
+
const matchedLiveSessionRecords = collectLiveMeshSessionRecords({
|
|
5972
|
+
meshId,
|
|
5973
|
+
node,
|
|
5974
|
+
nodeId,
|
|
5975
|
+
liveSessionRecords: liveMeshSessions,
|
|
5976
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
5977
|
+
});
|
|
5978
|
+
const workspace = readLiveMeshNodeWorkspace({
|
|
5979
|
+
meshId,
|
|
5980
|
+
nodeId,
|
|
5981
|
+
liveSessionRecords: matchedLiveSessionRecords,
|
|
5982
|
+
allowCoordinatorSession: nodeId === selectedCoordinatorNodeId,
|
|
5983
|
+
}) || (typeof node.workspace === 'string' ? node.workspace : '');
|
|
5984
|
+
status.workspace = workspace || node.workspace;
|
|
3094
5985
|
if (matchedLiveSessionRecords.length > 0) {
|
|
3095
5986
|
const sessionIds = matchedLiveSessionRecords
|
|
3096
5987
|
.map((record: any) => typeof record?.sessionId === 'string' ? record.sessionId : '')
|
|
@@ -3104,44 +5995,225 @@ export class DaemonCommandRouter {
|
|
|
3104
5995
|
status.providers = Array.from(new Set([...(Array.isArray(status.providers) ? status.providers as string[] : []), ...providerTypes]));
|
|
3105
5996
|
}
|
|
3106
5997
|
}
|
|
3107
|
-
if (
|
|
3108
|
-
if (!fs.existsSync(
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
if (
|
|
5998
|
+
if (workspace) {
|
|
5999
|
+
if (!fs.existsSync(workspace)) {
|
|
6000
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
6001
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
6002
|
+
let remoteProbeApplied = false;
|
|
6003
|
+
if (inlineTransitGit) {
|
|
6004
|
+
status.git = inlineTransitGit;
|
|
6005
|
+
status.health = inlineTransitGit.isGitRepo
|
|
6006
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
6007
|
+
: 'degraded';
|
|
6008
|
+
const connection = readObjectRecord(status.connection);
|
|
6009
|
+
const connectionState = readStringValue(connection.state);
|
|
6010
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
6011
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
6012
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
6013
|
+
}
|
|
6014
|
+
remoteProbeApplied = true;
|
|
6015
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
6016
|
+
try {
|
|
6017
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
6018
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6019
|
+
daemonId,
|
|
6020
|
+
workspace,
|
|
6021
|
+
timeoutMs: 8000,
|
|
6022
|
+
});
|
|
6023
|
+
if (remoteGit) {
|
|
6024
|
+
status.git = remoteGit;
|
|
6025
|
+
status.health = remoteGit.isGitRepo
|
|
6026
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
6027
|
+
: 'degraded';
|
|
6028
|
+
const connection = readObjectRecord(status.connection);
|
|
6029
|
+
const connectionState = readStringValue(connection.state);
|
|
6030
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
6031
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
6032
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
6033
|
+
}
|
|
6034
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
6035
|
+
remoteProbeApplied = true;
|
|
6036
|
+
}
|
|
6037
|
+
} catch {
|
|
6038
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
6039
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
6040
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
6041
|
+
status.connection = refreshedConnection;
|
|
6042
|
+
try {
|
|
6043
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
6044
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6045
|
+
daemonId,
|
|
6046
|
+
workspace,
|
|
6047
|
+
timeoutMs: 12000,
|
|
6048
|
+
});
|
|
6049
|
+
if (remoteGit) {
|
|
6050
|
+
status.git = remoteGit;
|
|
6051
|
+
status.health = remoteGit.isGitRepo
|
|
6052
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
6053
|
+
: 'degraded';
|
|
6054
|
+
const connection = readObjectRecord(status.connection);
|
|
6055
|
+
const connectionState = readStringValue(connection.state);
|
|
6056
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
6057
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
6058
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
6059
|
+
}
|
|
6060
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
6061
|
+
remoteProbeApplied = true;
|
|
6062
|
+
}
|
|
6063
|
+
} catch {
|
|
6064
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
if (!remoteProbeApplied) {
|
|
6070
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
6071
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
6072
|
+
&& !isSelfNode
|
|
6073
|
+
&& !!daemonId
|
|
6074
|
+
&& (
|
|
6075
|
+
readStringValue(status.machineStatus) === 'online'
|
|
6076
|
+
|| readStringValue(status.health) === 'online'
|
|
6077
|
+
|| connectionState === 'connecting'
|
|
6078
|
+
|| connectionState === 'connected'
|
|
6079
|
+
|| connectionState === 'unknown'
|
|
6080
|
+
);
|
|
6081
|
+
if (pendingPeerGitProbe) {
|
|
6082
|
+
status.gitProbePending = true;
|
|
6083
|
+
status.health = 'unknown';
|
|
6084
|
+
}
|
|
6085
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
6086
|
+
status,
|
|
6087
|
+
node,
|
|
6088
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
6089
|
+
)) {
|
|
6090
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
6091
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
6092
|
+
nodeStatuses.push(status);
|
|
6093
|
+
continue;
|
|
6094
|
+
}
|
|
6095
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
6096
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
6097
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
6098
|
+
nodeStatuses.push(status);
|
|
6099
|
+
continue;
|
|
6100
|
+
}
|
|
3121
6101
|
}
|
|
3122
|
-
}
|
|
3123
|
-
|
|
3124
|
-
|
|
6102
|
+
} else {
|
|
6103
|
+
try {
|
|
6104
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
6105
|
+
status.git = gitStatus;
|
|
6106
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
6107
|
+
if (gitStatus.isGitRepo) {
|
|
6108
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
6109
|
+
} else {
|
|
6110
|
+
status.health = 'degraded';
|
|
6111
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
6112
|
+
}
|
|
6113
|
+
} catch {
|
|
6114
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
6115
|
+
status.health = 'degraded';
|
|
6116
|
+
}
|
|
3125
6117
|
}
|
|
3126
6118
|
}
|
|
3127
6119
|
} else {
|
|
3128
6120
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
3129
6121
|
}
|
|
3130
|
-
|
|
6122
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
6123
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
3131
6124
|
nodeStatuses.push(status);
|
|
3132
6125
|
}
|
|
3133
6126
|
|
|
3134
|
-
|
|
6127
|
+
// (B3) Resolve the coordinator daemon scope for the drain.
|
|
6128
|
+
// Emit-time always tags events with the worker's
|
|
6129
|
+
// targetCoordinatorDaemonId and writes them to the
|
|
6130
|
+
// scoped pending-events file. If the caller (MCP tool,
|
|
6131
|
+
// dashboard, etc.) doesn't tell us which coordinator
|
|
6132
|
+
// they're running under, fall back to *this daemon's*
|
|
6133
|
+
// ID — the caller reached us over our IPC/WS, so they
|
|
6134
|
+
// are by definition under this daemon. Falling back to
|
|
6135
|
+
// undefined would silently miss every scoped event.
|
|
6136
|
+
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
6137
|
+
? args.coordinatorDaemonId.trim()
|
|
6138
|
+
: (this.deps.statusInstanceId || undefined);
|
|
6139
|
+
const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
|
|
6140
|
+
const previewFreshness = (() => {
|
|
6141
|
+
const localRepoRoot = nodeStatuses
|
|
6142
|
+
.map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
|
|
6143
|
+
.find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
|
|
6144
|
+
return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
|
|
6145
|
+
})();
|
|
6146
|
+
const asyncRefineJobs = buildMeshAsyncRefineJobs({
|
|
6147
|
+
meshId,
|
|
6148
|
+
ledgerEntries: asyncRefineLedgerEntries,
|
|
6149
|
+
pendingEvents: pendingCoordinatorEvents,
|
|
6150
|
+
});
|
|
6151
|
+
const historicalSessions = buildHistoricalMeshSessions({
|
|
6152
|
+
meshId,
|
|
6153
|
+
nodes: mesh.nodes || [],
|
|
6154
|
+
liveSessionRecords: liveMeshSessions,
|
|
6155
|
+
});
|
|
6156
|
+
const statusResult = {
|
|
3135
6157
|
success: true,
|
|
3136
6158
|
meshId: mesh.id,
|
|
3137
6159
|
meshName: mesh.name,
|
|
3138
6160
|
repoIdentity: mesh.repoIdentity,
|
|
3139
6161
|
defaultBranch: mesh.defaultBranch,
|
|
3140
|
-
refreshedAt
|
|
6162
|
+
refreshedAt,
|
|
6163
|
+
meshHost,
|
|
6164
|
+
sourceOfTruth: {
|
|
6165
|
+
membership: meshRecord?.source === 'inline_cache'
|
|
6166
|
+
? 'coordinator_inline_mesh_cache'
|
|
6167
|
+
: meshRecord?.source === 'local_config'
|
|
6168
|
+
? 'local_mesh_config'
|
|
6169
|
+
: 'inline_bootstrap_snapshot',
|
|
6170
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
6171
|
+
meshHost: {
|
|
6172
|
+
owner: 'mesh_host_daemon',
|
|
6173
|
+
localRole: meshHost.role,
|
|
6174
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
6175
|
+
hostNodeId: meshHost.hostNodeId,
|
|
6176
|
+
hostAddress: meshHost.hostAddress,
|
|
6177
|
+
},
|
|
6178
|
+
...(requireDirectPeerTruth ? {
|
|
6179
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
6180
|
+
directPeerTruth: {
|
|
6181
|
+
required: true,
|
|
6182
|
+
satisfied: directTruthSatisfied,
|
|
6183
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
6184
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
6185
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
6186
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
6187
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
6188
|
+
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
6189
|
+
},
|
|
6190
|
+
} : {}),
|
|
6191
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
|
|
6192
|
+
},
|
|
6193
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
6194
|
+
...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
|
|
3141
6195
|
nodes: nodeStatuses,
|
|
3142
6196
|
queue: { tasks: queue, summary: queueSummary },
|
|
3143
6197
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
6198
|
+
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
6199
|
+
...(historicalSessions ? { historicalSessions } : {}),
|
|
6200
|
+
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
3144
6201
|
};
|
|
6202
|
+
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
|
|
6203
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
6204
|
+
const returnedStatus = pendingCoordinatorEvents.length > 0
|
|
6205
|
+
? { ...rememberedStatus, pendingCoordinatorEvents }
|
|
6206
|
+
: rememberedStatus;
|
|
6207
|
+
logRepoMeshStatusDebug('return_live', {
|
|
6208
|
+
meshId,
|
|
6209
|
+
command: 'mesh_status',
|
|
6210
|
+
refreshRequested,
|
|
6211
|
+
refreshReason,
|
|
6212
|
+
meshSource: meshRecord.source,
|
|
6213
|
+
directTruth,
|
|
6214
|
+
summary: summarizeRepoMeshStatusDebug(returnedStatus),
|
|
6215
|
+
});
|
|
6216
|
+
return returnedStatus;
|
|
3145
6217
|
} catch (e: any) {
|
|
3146
6218
|
return { success: false, error: e.message };
|
|
3147
6219
|
}
|
|
@@ -3199,7 +6271,7 @@ export class DaemonCommandRouter {
|
|
|
3199
6271
|
|
|
3200
6272
|
// 3. Kill OS process if requested
|
|
3201
6273
|
if (killProcess) {
|
|
3202
|
-
const running = isIdeRunning(ideType);
|
|
6274
|
+
const running = await isIdeRunning(ideType);
|
|
3203
6275
|
if (running) {
|
|
3204
6276
|
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
|
|
3205
6277
|
const killed = await killIdeProcess(ideType);
|