fix(agentctl): add mesh-cluster and mesh-helpers modules
Restore missing mesh wiring so HeadlessSession and agentctl smokes load without MODULE_NOT_FOUND; host/guest mesh group runs invite join sync. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -3,6 +3,13 @@
|
|||||||
const { AgentServer } = require('./server')
|
const { AgentServer } = require('./server')
|
||||||
const { AgentClient } = require('./client')
|
const { AgentClient } = require('./client')
|
||||||
const { HeadlessSession } = require('./headless')
|
const { HeadlessSession } = require('./headless')
|
||||||
|
const { PearcordMeshCluster } = require('./mesh-cluster')
|
||||||
|
const {
|
||||||
|
connectGuildMeshGroup,
|
||||||
|
createIsolatedMeshDht,
|
||||||
|
waitFor,
|
||||||
|
sleep
|
||||||
|
} = require('./mesh-helpers')
|
||||||
const { matchView, getPath } = require('./assert')
|
const { matchView, getPath } = require('./assert')
|
||||||
const {
|
const {
|
||||||
resolveStoragePath,
|
resolveStoragePath,
|
||||||
@@ -17,6 +24,11 @@ module.exports = {
|
|||||||
AgentServer,
|
AgentServer,
|
||||||
AgentClient,
|
AgentClient,
|
||||||
HeadlessSession,
|
HeadlessSession,
|
||||||
|
PearcordMeshCluster,
|
||||||
|
connectGuildMeshGroup,
|
||||||
|
createIsolatedMeshDht,
|
||||||
|
waitFor,
|
||||||
|
sleep,
|
||||||
matchView,
|
matchView,
|
||||||
getPath,
|
getPath,
|
||||||
resolveStoragePath,
|
resolveStoragePath,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('bare-path')
|
||||||
|
const { HeadlessSession } = require('./headless')
|
||||||
|
const { connectGuildMeshGroup } = require('./mesh')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a scenario on multiple isolated headless peers (P2P mesh smoke / agent journeys).
|
||||||
|
*/
|
||||||
|
class PearcordMeshCluster {
|
||||||
|
constructor (opts = {}) {
|
||||||
|
this.peerCount = Math.max(2, Number(opts.peerCount) || 2)
|
||||||
|
this.sessions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
async runScenario (scenarioPath) {
|
||||||
|
return connectGuildMeshGroup(path.resolve(scenarioPath), {
|
||||||
|
peerCount: this.peerCount,
|
||||||
|
sessions: this.sessions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async close () {
|
||||||
|
for (const s of this.sessions) {
|
||||||
|
try {
|
||||||
|
await s.close()
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sessions.length = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { PearcordMeshCluster }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
function sleep (ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitFor (fn, timeoutMs = 15000, intervalMs = 50) {
|
||||||
|
const deadline = Date.now() + timeoutMs
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (await fn()) return true
|
||||||
|
await sleep(intervalMs)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {null} isolated DHT wiring is optional for headless IPC smokes */
|
||||||
|
function createIsolatedMeshDht () {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sleep,
|
||||||
|
waitFor,
|
||||||
|
createIsolatedMeshDht
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('bare-path')
|
||||||
|
const os = require('bare-os')
|
||||||
|
const { HeadlessSession } = require('./headless')
|
||||||
|
const { waitFor } = require('./mesh-helpers')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host + guest headless sessions run the same onboard scenario shape, then exchange a mesh marker message.
|
||||||
|
* @param {string} scenarioPath path to JSON with host steps (register + create-guild)
|
||||||
|
* @param {{ peerCount?: number, sessions?: HeadlessSession[] }} opts
|
||||||
|
*/
|
||||||
|
async function connectGuildMeshGroup (scenarioPath, opts = {}) {
|
||||||
|
const peerCount = Math.max(2, Number(opts.peerCount) || 2)
|
||||||
|
const base = path.join(os.tmpdir(), `pearcord-agentctl-mesh-${Date.now()}`)
|
||||||
|
const host = new HeadlessSession({ storagePath: path.join(base, 'host') })
|
||||||
|
const guest = new HeadlessSession({ storagePath: path.join(base, 'guest') })
|
||||||
|
if (opts.sessions) {
|
||||||
|
opts.sessions.push(host, guest)
|
||||||
|
}
|
||||||
|
await host.start()
|
||||||
|
await guest.start()
|
||||||
|
try {
|
||||||
|
await host.runScenario(scenarioPath)
|
||||||
|
const invite = host.lastView?.lastInvite?.code || host.lastView?.inviteCode
|
||||||
|
if (!invite) {
|
||||||
|
const created = await host.ipc({ type: 'create-invite' })
|
||||||
|
const code = created?.lastInvite?.code
|
||||||
|
if (!code) throw new Error('mesh: no invite code after host scenario')
|
||||||
|
await guest.ipc({ type: 'join-invite', code })
|
||||||
|
} else {
|
||||||
|
await guest.ipc({ type: 'join-invite', code: invite })
|
||||||
|
}
|
||||||
|
await waitFor(async () => guest.lastView?.mode === 'guild', 20000)
|
||||||
|
const token = `agentctl-mesh-${Date.now()}`
|
||||||
|
await host.ipc({ type: 'send-message', content: token })
|
||||||
|
const synced = await waitFor(async () => {
|
||||||
|
const v = guest.lastView
|
||||||
|
return (v?.messages || []).some((m) => String(m.content || '').includes(token))
|
||||||
|
}, 25000)
|
||||||
|
if (!synced) throw new Error('mesh: guest did not receive host message')
|
||||||
|
return {
|
||||||
|
peerCount,
|
||||||
|
hostPeers: host.lastView?.stats?.peers ?? 0,
|
||||||
|
guestPeers: guest.lastView?.stats?.peers ?? 0,
|
||||||
|
token
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await host.close()
|
||||||
|
await guest.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
connectGuildMeshGroup
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user