feat(search): Phase 627 search hub depth (v0.8.590)
Extend search hub/panel live regions with active channel context and mesh notes; tighten search-errors reaction bleed exclusion; refresh composer search hints for hub and panel; platform guild.search.cache/mesh spans include activeChannelId; agentctl and phase627 smoke bundle.
This commit is contained in:
@@ -107,6 +107,8 @@ Both modes use **`pearcord-ui-flow` `dispatchUiMessage`** — the same handler a
|
|||||||
|
|
||||||
**v0.8.521 (Phase 558):** Headless `joinGuildVoiceChannel`, `leaveGuildVoiceChannel`, and `setGuildVoiceMute` call `refreshView` after mutations. Bundle: `npm run test:agentctl-phase558-voice` (chains `test:agentctl-phase524-voice`).
|
**v0.8.521 (Phase 558):** Headless `joinGuildVoiceChannel`, `leaveGuildVoiceChannel`, and `setGuildVoiceMute` call `refreshView` after mutations. Bundle: `npm run test:agentctl-phase558-voice` (chains `test:agentctl-phase524-voice`).
|
||||||
|
|
||||||
|
**v0.8.590 (Phase 627):** Headless `searchGuildMessages` `refreshView` re-verified for search hub depth bundle. Bundle: `npm run test:agentctl-phase627-search` (chains `test:agentctl-phase613-search` + `smoke-agentctl-search-phase627`).
|
||||||
|
|
||||||
**v0.8.589 (Phase 626):** Headless `toggleGuildReaction` `refreshView` re-verified for reaction hub depth bundle. Bundle: `npm run test:agentctl-phase626-reactions` (chains `test:agentctl-phase612-reactions` + `smoke-agentctl-reaction-phase626`).
|
**v0.8.589 (Phase 626):** Headless `toggleGuildReaction` `refreshView` re-verified for reaction hub depth bundle. Bundle: `npm run test:agentctl-phase626-reactions` (chains `test:agentctl-phase612-reactions` + `smoke-agentctl-reaction-phase626`).
|
||||||
|
|
||||||
**v0.8.588 (Phase 625):** Headless `createThreadFromLastMessage` `refreshView` re-verified for threads hub depth bundle. Bundle: `npm run test:agentctl-phase625-threads` (chains `test:agentctl-phase611-threads` + `smoke-agentctl-threads-phase625`).
|
**v0.8.588 (Phase 625):** Headless `createThreadFromLastMessage` `refreshView` re-verified for threads hub depth bundle. Bundle: `npm run test:agentctl-phase625-threads` (chains `test:agentctl-phase611-threads` + `smoke-agentctl-threads-phase625`).
|
||||||
|
|||||||
@@ -13,6 +13,34 @@ async function waitFor (fn, timeoutMs = 15000, intervalMs = 50) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-join host/guest guild Hyperswarm keypairs and wait until both report mesh peers.
|
||||||
|
* @param {import('pearcord-platform').PearcordPlatform} host
|
||||||
|
* @param {import('pearcord-platform').PearcordPlatform} guest
|
||||||
|
*/
|
||||||
|
async function connectGuildMeshPeers (host, guest, timeoutMs = 28000) {
|
||||||
|
if (!host?.guild?.swarm || !guest?.guild?.swarm) {
|
||||||
|
throw new Error('host and guest must be on an active guild mesh')
|
||||||
|
}
|
||||||
|
const hostPk = host.guild.swarm.keyPair.publicKey
|
||||||
|
const guestPk = guest.guild.swarm.keyPair.publicKey
|
||||||
|
guest.guild.swarm.joinPeer(hostPk)
|
||||||
|
host.guild.swarm.joinPeer(guestPk)
|
||||||
|
await guest.guild.swarm.flush()
|
||||||
|
await host.guild.swarm.flush()
|
||||||
|
const ok = await waitFor(async () => {
|
||||||
|
const h = await host.view()
|
||||||
|
const g = await guest.view()
|
||||||
|
return (h.stats?.peers || 0) >= 1 && (g.stats?.peers || 0) >= 1
|
||||||
|
}, timeoutMs, 350)
|
||||||
|
if (!ok) throw new Error('peers did not connect on guild mesh')
|
||||||
|
await sleep(1200)
|
||||||
|
return {
|
||||||
|
hostPeers: (await host.view()).stats?.peers,
|
||||||
|
guestPeers: (await guest.view()).stats?.peers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns {null} isolated DHT wiring is optional for headless IPC smokes */
|
/** @returns {null} isolated DHT wiring is optional for headless IPC smokes */
|
||||||
function createIsolatedMeshDht () {
|
function createIsolatedMeshDht () {
|
||||||
return null
|
return null
|
||||||
@@ -21,5 +49,6 @@ function createIsolatedMeshDht () {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
sleep,
|
sleep,
|
||||||
waitFor,
|
waitFor,
|
||||||
|
connectGuildMeshPeers,
|
||||||
createIsolatedMeshDht
|
createIsolatedMeshDht
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const fs = require('bare-fs')
|
|||||||
const path = require('bare-path')
|
const path = require('bare-path')
|
||||||
const os = require('bare-os')
|
const os = require('bare-os')
|
||||||
const { HeadlessSession } = require('./headless')
|
const { HeadlessSession } = require('./headless')
|
||||||
const { waitFor } = require('./mesh-helpers')
|
const { waitFor, connectGuildMeshPeers } = require('./mesh-helpers')
|
||||||
|
|
||||||
async function ensureGuestOnboarded (guest) {
|
async function ensureGuestOnboarded (guest) {
|
||||||
await guest.refreshView()
|
await guest.refreshView()
|
||||||
@@ -28,6 +28,34 @@ async function resolveHostInviteCode (host) {
|
|||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureGuestActiveTextChannel (guest) {
|
||||||
|
await guest.refreshView()
|
||||||
|
if (guest.lastView?.activeChannelId) return
|
||||||
|
const ch = (guest.lastView?.channels || []).find(
|
||||||
|
(c) => c.type === 'text' || c.type === 'announcement'
|
||||||
|
)
|
||||||
|
if (!ch) return
|
||||||
|
await guest.ipc({ type: 'select-channel', channelId: ch.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForGuestMeshMessage (host, guest, token, opts = {}) {
|
||||||
|
const attempts = Math.max(1, Number(opts.attempts) || 8)
|
||||||
|
const waitMs = Number(opts.waitMs) || 8000
|
||||||
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
||||||
|
await host.ipc({
|
||||||
|
type: 'send-message',
|
||||||
|
content: `${token} attempt ${attempt}`
|
||||||
|
})
|
||||||
|
const got = await waitFor(async () => {
|
||||||
|
await guest.refreshView()
|
||||||
|
const v = guest.lastView
|
||||||
|
return (v?.messages || []).some((m) => String(m.content || '').includes(token))
|
||||||
|
}, waitMs, 350)
|
||||||
|
if (got) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Host + guest headless sessions run the same onboard scenario shape, then exchange a mesh marker message.
|
* 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 {string} scenarioPath path to JSON with host steps (register + create-guild)
|
||||||
@@ -49,17 +77,17 @@ async function connectGuildMeshGroup (scenarioPath, opts = {}) {
|
|||||||
await ensureGuestOnboarded(guest)
|
await ensureGuestOnboarded(guest)
|
||||||
await guest.ipc({ type: 'join-invite', code: invite })
|
await guest.ipc({ type: 'join-invite', code: invite })
|
||||||
await waitFor(async () => guest.lastView?.mode === 'guild', 20000)
|
await waitFor(async () => guest.lastView?.mode === 'guild', 20000)
|
||||||
|
await ensureGuestActiveTextChannel(guest)
|
||||||
|
const mesh = await connectGuildMeshPeers(host.platform, guest.platform, opts.meshTimeoutMs || 45000)
|
||||||
const token = opts.messageContent || `agentctl-mesh-${Date.now()}`
|
const token = opts.messageContent || `agentctl-mesh-${Date.now()}`
|
||||||
await host.ipc({ type: 'send-message', content: token })
|
const synced = await waitForGuestMeshMessage(host, guest, token, opts)
|
||||||
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')
|
if (!synced) throw new Error('mesh: guest did not receive host message')
|
||||||
|
await host.refreshView()
|
||||||
|
await guest.refreshView()
|
||||||
return {
|
return {
|
||||||
peerCount,
|
peerCount,
|
||||||
hostPeers: host.lastView?.stats?.peers ?? 0,
|
hostPeers: mesh.hostPeers ?? host.lastView?.stats?.peers ?? 0,
|
||||||
guestPeers: guest.lastView?.stats?.peers ?? 0,
|
guestPeers: mesh.guestPeers ?? guest.lastView?.stats?.peers ?? 0,
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -91,15 +119,27 @@ async function connectGuildMeshThread (scenarioPath, opts = {}) {
|
|||||||
await ensureGuestOnboarded(guest)
|
await ensureGuestOnboarded(guest)
|
||||||
await guest.ipc({ type: 'join-invite', code: invite })
|
await guest.ipc({ type: 'join-invite', code: invite })
|
||||||
await waitFor(async () => guest.lastView?.mode === 'guild', 20000)
|
await waitFor(async () => guest.lastView?.mode === 'guild', 20000)
|
||||||
|
await ensureGuestActiveTextChannel(guest)
|
||||||
|
const mesh = await connectGuildMeshPeers(host.platform, guest.platform, opts.meshTimeoutMs || 45000)
|
||||||
const rootMsg = (host.lastView?.messages || [])[0]
|
const rootMsg = (host.lastView?.messages || [])[0]
|
||||||
if (!rootMsg?.id) throw new Error('thread mesh: host has no root message')
|
if (!rootMsg?.id) throw new Error('thread mesh: host has no root message')
|
||||||
await host.ipc({ type: 'create-thread', messageId: rootMsg.id, name: 'Mesh Thread' })
|
await host.ipc({ type: 'create-thread', messageId: rootMsg.id, name: 'Mesh Thread' })
|
||||||
await waitFor(async () => host.lastView?.activeChannel?.type === 'thread', 10000)
|
await waitFor(async () => host.lastView?.activeChannel?.type === 'thread', 10000)
|
||||||
await host.ipc({ type: 'send-message', content: replyToken })
|
const threadId = host.lastView?.activeChannelId
|
||||||
const synced = await waitFor(async () => {
|
if (threadId) {
|
||||||
const v = guest.lastView
|
const hasThread = await waitFor(async () => {
|
||||||
return (v?.messages || []).some((m) => String(m.content || '').includes(replyToken))
|
await guest.refreshView()
|
||||||
}, 60000)
|
return (guest.lastView?.channels || []).some((c) => c.id === threadId)
|
||||||
|
}, 30000, 350)
|
||||||
|
if (hasThread) {
|
||||||
|
await guest.ipc({ type: 'select-channel', channelId: threadId })
|
||||||
|
await waitFor(async () => guest.lastView?.activeChannelId === threadId, 10000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const synced = await waitForGuestMeshMessage(host, guest, replyToken, {
|
||||||
|
attempts: 10,
|
||||||
|
waitMs: 10000
|
||||||
|
})
|
||||||
if (!synced) throw new Error('thread mesh: guest did not receive thread reply')
|
if (!synced) throw new Error('thread mesh: guest did not receive thread reply')
|
||||||
if (spec.markThreadAndParentRead) {
|
if (spec.markThreadAndParentRead) {
|
||||||
await host.runSteps([{ markThreadAndParentRead: true }])
|
await host.runSteps([{ markThreadAndParentRead: true }])
|
||||||
@@ -109,8 +149,8 @@ async function connectGuildMeshThread (scenarioPath, opts = {}) {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
peerCount,
|
peerCount,
|
||||||
hostPeers: host.lastView?.stats?.peers ?? 0,
|
hostPeers: mesh.hostPeers ?? host.lastView?.stats?.peers ?? 0,
|
||||||
guestPeers: guest.lastView?.stats?.peers ?? 0,
|
guestPeers: mesh.guestPeers ?? guest.lastView?.stats?.peers ?? 0,
|
||||||
replyToken
|
replyToken
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user