Add features across agents, collab, trust, indexes, and hyperbee.

Manual pass: task cancelTask, workflow topologicalOrder/fail/reset, collab kick/replay, trust trustedPeers and multisig TTL sweep, graph shortestPath, inverted topTerms, dedup filterNew, bee notifyBatch/seekVersion — all with tests.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-21 01:04:42 -04:00
co-authored by Cursor
parent cb9caad46c
commit b8b815fce6
25 changed files with 386 additions and 6 deletions
+2 -2
View File
@@ -15,8 +15,8 @@ Autonomous agent primitives: persistent causal memory, DAG task orchestration, a
| Module | Protocol | Summary | | Module | Protocol | Summary |
|--------|----------|---------| |--------|----------|---------|
| [hyper-p2p-agent-memory](./hyper-p2p-agent-memory/) | `hyper-p2p-agent-memory/v1` | Episodic/semantic memory, tags, causal links, Hyperbee | | [hyper-p2p-agent-memory](./hyper-p2p-agent-memory/) | `hyper-p2p-agent-memory/v1` | Episodic/semantic memory, tags, causal links, Hyperbee |
| [hyper-p2p-task-orchestrator](./hyper-p2p-task-orchestrator/) | `hyper-p2p-task-orchestrator/v1` | Signed tasks, `taskCounts()`, vector-clock lifecycle | | [hyper-p2p-task-orchestrator](./hyper-p2p-task-orchestrator/) | `hyper-p2p-task-orchestrator/v1` | `cancelTask`, `listTaskIds`, signed DAG tasks |
| [hyper-p2p-workflow-graph](./hyper-p2p-workflow-graph/) | `workflow-graph/v1` | DAG nodes/edges, `readyNodes()`, cycle detection | | [hyper-p2p-workflow-graph](./hyper-p2p-workflow-graph/) | `workflow-graph/v1` | `topologicalOrder`, `fail`, `reset`, cycle-safe DAG |
## Quick start ## Quick start
@@ -14,7 +14,13 @@ DAG task orchestration with signed tasks, vector-clock lifecycle, optional Hyper
### `registerHandler(taskType, fn)` / `getTask(taskId)` / `getResult(taskId)` ### `registerHandler(taskType, fn)` / `getTask(taskId)` / `getResult(taskId)`
### `taskCounts() → { pending, running, completed, failed, total }` ### `cancelTask(taskId, reason?) → task`
Marks non-terminal tasks as failed with `cancelReason`.
### `listTaskIds() → string[]`
### `taskCounts() → { pending, assigned, completed, failed }`
### `getStats() → { ...metrics, ...taskCounts(), protocol }` ### `getStats() → { ...metrics, ...taskCounts(), protocol }`
@@ -342,6 +342,27 @@ class HyperP2PTaskOrchestrator extends EventEmitter {
return this.tasks.get(taskId) || null return this.tasks.get(taskId) || null
} }
async cancelTask (taskId, reason = '') {
await this.ready()
const task = this.tasks.get(taskId)
if (!task) throw new Error('Task not found')
if (task.status === 'completed' || task.status === 'failed') {
throw new Error('Task already terminal')
}
task.status = 'failed'
task.cancelledAt = Date.now()
task.cancelReason = reason
this.metrics.tasksFailed++
await this._persist()
this.emit('task:cancelled', task)
this._gossipTask(task)
return task
}
listTaskIds () {
return [...this.tasks.keys()]
}
async queryTasks (filter = {}) { async queryTasks (filter = {}) {
await this.ready() await this.ready()
let results = Array.from(this.tasks.values()) let results = Array.from(this.tasks.values())
@@ -146,3 +146,15 @@ test('hyper-p2p-task-orchestrator: validation rejects invalid input', async (t)
} }
await m.close() await m.close()
}) })
test('cancelTask and listTaskIds', async (t) => {
const keyPair = require('hypercore-crypto').keyPair()
const orch = new HyperP2PTaskOrchestrator({ keyPair, storageDir: '/tmp/task-orch-cancel-' + Date.now() })
await orch.ready()
const id = await orch.submitTask({ type: 'x', payload: {} })
t.ok(orch.listTaskIds().includes(id))
await orch.cancelTask(id, 'user abort')
const task = await orch.getTask(id)
t.is(task.status, 'failed')
await orch.close()
})
@@ -95,12 +95,66 @@ class HyperP2PWorkflowGraph extends EventEmitter {
return [...this._nodes.values()].filter((n) => n.state === 'pending').length return [...this._nodes.values()].filter((n) => n.state === 'pending').length
} }
failedCount () {
return [...this._nodes.values()].filter((n) => n.state === 'failed').length
}
isComplete () { isComplete () {
return this._nodes.size > 0 && this.pendingCount() === 0 return this._nodes.size > 0 && this.pendingCount() === 0 && this.failedCount() === 0
} }
listNodes () { return [...this._nodes.values()] } listNodes () { return [...this._nodes.values()] }
topologicalOrder () {
const inDeg = new Map()
for (const id of this._nodes.keys()) inDeg.set(id, 0)
for (const e of this._edges) {
inDeg.set(e.to, (inDeg.get(e.to) || 0) + 1)
}
const q = []
for (const [id, d] of inDeg) if (d === 0) q.push(id)
const order = []
const adj = new Map()
for (const e of this._edges) {
if (!adj.has(e.from)) adj.set(e.from, [])
adj.get(e.from).push(e.to)
}
while (q.length) {
const id = q.shift()
order.push(id)
for (const next of adj.get(id) || []) {
inDeg.set(next, inDeg.get(next) - 1)
if (inDeg.get(next) === 0) q.push(next)
}
}
if (order.length !== this._nodes.size) {
throw new Error('workflow graph has cycle')
}
return order
}
fail (id, reason = '') {
const node = this._nodes.get(id)
if (!node) return false
node.state = 'failed'
node.failedAt = Date.now()
node.failReason = reason
if (this._peerMsgs) gossipSend(this, { type: 'fail', id, reason })
this.emit('fail', node)
return true
}
reset (id) {
const node = this._nodes.get(id)
if (!node) return false
node.state = 'pending'
delete node.completedAt
delete node.failedAt
if (this._peerMsgs) gossipSend(this, { type: 'reset', id })
this.emit('reset', node)
return true
}
toJSON () { toJSON () {
return { nodes: [...this._nodes.values()], edges: [...this._edges] } return { nodes: [...this._nodes.values()], edges: [...this._edges] }
} }
@@ -135,6 +189,8 @@ class HyperP2PWorkflowGraph extends EventEmitter {
if (data && data.type === 'node' && data.node) this.merge({ nodes: [data.node], edges: [] }) if (data && data.type === 'node' && data.node) this.merge({ nodes: [data.node], edges: [] })
else if (data && data.type === 'edge' && data.edge) this.merge({ nodes: [], edges: [data.edge] }) else if (data && data.type === 'edge' && data.edge) this.merge({ nodes: [], edges: [data.edge] })
else if (data && data.type === 'complete' && data.id) this.complete(data.id) else if (data && data.type === 'complete' && data.id) this.complete(data.id)
else if (data && data.type === 'fail' && data.id) this.fail(data.id, data.reason)
else if (data && data.type === 'reset' && data.id) this.reset(data.id)
} }
}) })
return this return this
@@ -147,6 +203,7 @@ class HyperP2PWorkflowGraph extends EventEmitter {
nodes: this._nodes.size, nodes: this._nodes.size,
edges: this._edges.length, edges: this._edges.length,
pending: this.pendingCount(), pending: this.pendingCount(),
failed: this.failedCount(),
protocol: PROTOCOL protocol: PROTOCOL
} }
} }
@@ -13,6 +13,19 @@ test('workflow-graph: DAG ready nodes', async (t) => {
await w.close() await w.close()
}) })
test('workflow-graph: topologicalOrder fail reset', async (t) => {
const w = new HyperP2PWorkflowGraph()
w.addNode('a')
w.addNode('b')
w.addEdge('a', 'b')
t.alike(w.topologicalOrder(), ['a', 'b'])
w.fail('a', 'err')
t.is(w.failedCount(), 1)
t.ok(w.reset('a'))
t.is(w.pendingCount(), 2)
await w.close()
})
test('workflow-graph: cycle rejected', async (t) => { test('workflow-graph: cycle rejected', async (t) => {
const w = new HyperP2PWorkflowGraph() const w = new HyperP2PWorkflowGraph()
w.addNode('x') w.addNode('x')
+2 -1
View File
@@ -8,7 +8,8 @@ Real-time collab primitives: rooms, cursors, line locks, and whiteboard ops over
| Module | Protocol | Highlights | | Module | Protocol | Highlights |
|--------|----------|------------| |--------|----------|------------|
| [hyper-p2p-collab-room](./hyper-p2p-collab-room/) | `collab-room/v1` | `listRooms()`, `getMembers`, broadcast | | [hyper-p2p-collab-room](./hyper-p2p-collab-room/) | `collab-room/v1` | `kickMember`, `isMember`, `memberCount` |
| [hyper-p2p-whiteboard-op](./hyper-p2p-whiteboard-op/) | `whiteboard-op/v1` | `replay(fromSeq)`, `lastSeq` |
| [hyper-p2p-cursor-presence](./hyper-p2p-cursor-presence/) | `cursor-presence/v1` | Per-doc cursor map | | [hyper-p2p-cursor-presence](./hyper-p2p-cursor-presence/) | `cursor-presence/v1` | Per-doc cursor map |
| [hyper-p2p-document-line-lock](./hyper-p2p-document-line-lock/) | `document-line-lock/v1` | Line-level locks | | [hyper-p2p-document-line-lock](./hyper-p2p-document-line-lock/) | `document-line-lock/v1` | Line-level locks |
| [hyper-p2p-whiteboard-op](./hyper-p2p-whiteboard-op/) | `whiteboard-op/v1` | Op log + replay | | [hyper-p2p-whiteboard-op](./hyper-p2p-whiteboard-op/) | `whiteboard-op/v1` | Op log + replay |
@@ -93,6 +93,31 @@ class HyperP2PCollabRoom extends EventEmitter {
return room.events.slice(-limit) return room.events.slice(-limit)
} }
memberCount (roomId) {
const room = this._rooms.get(roomId)
return room ? room.members.size : 0
}
isMember (roomId, peer = null) {
const room = this._rooms.get(roomId)
if (!room) return false
return room.members.has(peer || this.peerHex)
}
kickMember (roomId, peer) {
assertNonEmpty(roomId, 'roomId')
assertNonEmpty(peer, 'peer')
const room = this._rooms.get(roomId)
if (!room || !room.members.has(peer)) return false
room.members.delete(peer)
if (this._peerMsgs) {
gossipSend(this, { type: 'kick', roomId, peer })
this._stats.gossipOut++
}
this.emit('kick', { roomId, peer })
return true
}
_onGossip (data) { _onGossip (data) {
if (!data || !data.type) return if (!data || !data.type) return
this._stats.gossipIn++ this._stats.gossipIn++
@@ -107,6 +132,10 @@ class HyperP2PCollabRoom extends EventEmitter {
room.members.delete(data.peer) room.members.delete(data.peer)
this.emit('peer-left', { roomId: data.roomId, peer: data.peer }) this.emit('peer-left', { roomId: data.roomId, peer: data.peer })
} }
if (data.type === 'kick' && data.peer) {
room.members.delete(data.peer)
this.emit('peer-kicked', { roomId: data.roomId, peer: data.peer })
}
if (data.type === 'broadcast' && data.event) { if (data.type === 'broadcast' && data.event) {
room.events.push(data.event) room.events.push(data.event)
this.emit('broadcast', { roomId: data.roomId, event: data.event, remote: true }) this.emit('broadcast', { roomId: data.roomId, event: data.event, remote: true })
@@ -47,3 +47,15 @@ test('getStats', async (t) => {
t.ok(room.getStats().protocol) t.ok(room.getStats().protocol)
await room.close() await room.close()
}) })
test('kickMember and isMember', async (t) => {
const room = new HyperP2PCollabRoom()
room.createRoom('k')
room.join('k')
room._onGossip({ type: 'join', roomId: 'k', member: { peer: 'bad', meta: {} } })
t.is(room.memberCount('k'), 2)
t.ok(room.kickMember('k', 'bad'))
t.is(room.memberCount('k'), 1)
t.ok(room.isMember('k'))
await room.close()
})
@@ -42,6 +42,17 @@ class HyperP2PWhiteboardOp extends EventEmitter {
return this._log.filter((e) => e.roomId === roomId).slice(-limit) return this._log.filter((e) => e.roomId === roomId).slice(-limit)
} }
replay (roomId, fromSeq = 0) {
assertNonEmpty(roomId, 'roomId')
const seq = Number(fromSeq) | 0
return this._log.filter((e) => e.roomId === roomId && e.seq >= seq)
}
lastSeq (roomId) {
const entries = this.history(roomId, 1)
return entries.length ? entries[0].seq : -1
}
mergeRemote (entry) { mergeRemote (entry) {
if (!entry || !entry.roomId) return false if (!entry || !entry.roomId) return false
this._stats.gossipIn++ this._stats.gossipIn++
@@ -37,3 +37,14 @@ test('getStats', async (t) => {
t.ok(wb.getStats().protocol) t.ok(wb.getStats().protocol)
await wb.close() await wb.close()
}) })
test('replay from seq', async (t) => {
const wb = new HyperP2PWhiteboardOp()
wb.apply('r', { n: 0 })
wb.apply('r', { n: 1 })
const replay = wb.replay('r', 1)
t.is(replay.length, 1)
t.is(replay[0].op.n, 1)
t.is(wb.lastSeq('r'), 1)
await wb.close()
})
@@ -103,6 +103,27 @@ class HyperP2PGraphIndex extends EventEmitter {
return true return true
} }
shortestPath (from, to, maxHops = 32) {
assertNonEmpty(from, 'from')
assertNonEmpty(to, 'to')
if (from === to) return [from]
const queue = [[from]]
const visited = new Set([from])
while (queue.length) {
const path = queue.shift()
if (path.length > maxHops) break
const node = path[path.length - 1]
for (const nbr of this.neighbors(node)) {
if (visited.has(nbr)) continue
const next = [...path, nbr]
if (nbr === to) return next
visited.add(nbr)
queue.push(next)
}
}
return null
}
getStats () { getStats () {
return { return {
...this._stats, ...this._stats,
@@ -47,3 +47,12 @@ test('removeEdge outDegree edgeCount', async (t) => {
t.is(m.outDegree('a'), 1) t.is(m.outDegree('a'), 1)
await m.close() await m.close()
}) })
test('shortestPath', async (t) => {
const m = new HyperP2PGraphIndex()
m.addEdge('a', 'b')
m.addEdge('b', 'c')
t.alike(m.shortestPath('a', 'c'), ['a', 'b', 'c'])
t.is(m.shortestPath('a', 'z'), null)
await m.close()
})
@@ -104,6 +104,13 @@ class HyperP2PInvertedIndex extends EventEmitter {
return this._docs.size return this._docs.size
} }
topTerms (limit = 10) {
const ranked = [...this._terms.entries()]
.map(([term, set]) => ({ term, docs: set.size }))
.sort((a, b) => b.docs - a.docs)
return ranked.slice(0, Math.max(0, limit | 0))
}
_gossip (data) { _gossip (data) {
if (!this._peerMsgs) return if (!this._peerMsgs) return
gossipSend(this, data) gossipSend(this, data)
@@ -48,3 +48,13 @@ test('searchAll getDocTerms', async (t) => {
t.alike(m.getDocTerms('d1'), ['cat', 'dog']) t.alike(m.getDocTerms('d1'), ['cat', 'dog'])
await m.close() await m.close()
}) })
test('topTerms', async (t) => {
const m = new HyperP2PInvertedIndex()
m.index('a', ['x'])
m.index('b', ['x', 'y'])
m.index('c', ['x'])
t.is(m.topTerms(1)[0].term, 'x')
t.is(m.topTerms(1)[0].docs, 3)
await m.close()
})
@@ -69,6 +69,22 @@ class HyperP2PDedupFilter extends EventEmitter {
this.emit('compact', { size: this._seen.size }) this.emit('compact', { size: this._seen.size })
} }
filterNew (ids) {
if (!Array.isArray(ids)) throw new Error('ids must be an array')
const out = []
for (const id of ids) {
if (!this.seen(id)) {
this.add(id)
out.push(id)
}
}
return out
}
listRecent (limit = 100) {
const arr = Array.from(this._seen)
return arr.slice(-Math.max(0, limit | 0))
}
getStats () { getStats () {
return { ...this._stats, seen: this._seen.size, maxIds: this.maxIds, protocol: PROTOCOL } return { ...this._stats, seen: this._seen.size, maxIds: this.maxIds, protocol: PROTOCOL }
@@ -16,6 +16,15 @@ test('dedup-filter: compact', async (t) => {
t.ok(f._seen.size <= 10) t.ok(f._seen.size <= 10)
await f.close() await f.close()
}) })
test('filterNew and listRecent', async (t) => {
const f = new HyperP2PDedupFilter()
f.add('old')
const fresh = f.filterNew(['old', 'new1', 'new2'])
t.alike(fresh, ['new1', 'new2'])
t.ok(f.listRecent(10).includes('new2'))
await f.close()
})
test('hyper-p2p-dedup-filter: close without leak', async (t) => { test('hyper-p2p-dedup-filter: close without leak', async (t) => {
const m = new HyperP2PDedupFilter() const m = new HyperP2PDedupFilter()
await m.close() await m.close()
@@ -81,6 +81,15 @@ class HyperP2PBeeDiffFollow extends EventEmitter {
return n return n
} }
seekVersion (version) {
const v = Number(version)
if (!Number.isFinite(v) || v < 0) throw new Error('version must be non-negative')
this._cursor = v
this._buffer = this._buffer.filter((e) => e.version > v)
this.emit('seek', { cursor: this._cursor })
return this._cursor
}
getStats () { getStats () {
return { return {
...this._stats, ...this._stats,
@@ -38,3 +38,13 @@ test('getStats', async (t) => {
t.is(m.getStats().protocol, 'bee-diff-follow/v1') t.is(m.getStats().protocol, 'bee-diff-follow/v1')
await m.close() await m.close()
}) })
test('seekVersion', async (t) => {
const m = new HyperP2PBeeDiffFollow()
m.recordDiff({ version: 1, key: 'a' })
m.recordDiff({ version: 5, key: 'b' })
m.seekVersion(3)
t.is(m.diffCursor(), 3)
t.is(m.pullDiff(10).length, 1)
await m.close()
})
@@ -50,6 +50,15 @@ class HyperP2PBeeRangeWatch extends EventEmitter {
return n return n
} }
notifyBatch (entries) {
if (!Array.isArray(entries)) throw new Error('entries must be an array')
let total = 0
for (const e of entries) {
if (e && e.key != null) total += this.emitChange(e.key, e.value, e.op || 'put')
}
return total
}
unwatch (id) { unwatch (id) {
const ok = this._watches.delete(id) const ok = this._watches.delete(id)
if (ok) this._stats.unwatch++ if (ok) this._stats.unwatch++
@@ -46,3 +46,16 @@ test('watch notify aliases listWatches', async (t) => {
t.is(m.listWatches().length, 1) t.is(m.listWatches().length, 1)
await m.close() await m.close()
}) })
test('notifyBatch', async (t) => {
const m = new HyperP2PBeeRangeWatch()
let hit = 0
m.watchRange('a', 'z', () => { hit++ })
const n = m.notifyBatch([
{ key: 'b', value: 1 },
{ key: 'c', value: 2 }
])
t.is(n, 2)
t.is(hit, 2)
await m.close()
})
@@ -16,7 +16,7 @@ class HyperP2PMultisigThreshold extends EventEmitter {
this._peerMsgs = null this._peerMsgs = null
} }
createProposal (id, signers, threshold) { createProposal (id, signers, threshold, opts = {}) {
assertNonEmpty(id, 'id') assertNonEmpty(id, 'id')
if (!Array.isArray(signers) || signers.length === 0) { if (!Array.isArray(signers) || signers.length === 0) {
throw new Error('signers must be a non-empty array') throw new Error('signers must be a non-empty array')
@@ -24,12 +24,14 @@ class HyperP2PMultisigThreshold extends EventEmitter {
if (threshold < 1 || threshold > signers.length) { if (threshold < 1 || threshold > signers.length) {
throw new Error('threshold must be between 1 and signers.length') throw new Error('threshold must be between 1 and signers.length')
} }
const expiresAt = opts.expiresAt ?? (opts.ttlMs ? Date.now() + opts.ttlMs : null)
const proposal = { const proposal = {
id, id,
signers: [...signers], signers: [...signers],
threshold, threshold,
signatures: new Set(), signatures: new Set(),
createdAt: Date.now(), createdAt: Date.now(),
expiresAt,
approved: false approved: false
} }
this._proposals.set(id, proposal) this._proposals.set(id, proposal)
@@ -66,9 +68,47 @@ class HyperP2PMultisigThreshold extends EventEmitter {
isApproved (id) { isApproved (id) {
assertNonEmpty(id, 'id') assertNonEmpty(id, 'id')
const p = this._proposals.get(id) const p = this._proposals.get(id)
if (p && p.expiresAt && Date.now() > p.expiresAt) return false
return !!(p && p.approved) return !!(p && p.approved)
} }
signatureCount (id) {
const p = this._proposals.get(id)
return p ? p.signatures.size : 0
}
listProposals () {
return [...this._proposals.values()].map((p) => ({
id: p.id,
signers: p.signers,
threshold: p.threshold,
signatures: p.signatures.size,
approved: p.approved,
expiresAt: p.expiresAt,
createdAt: p.createdAt
}))
}
expireProposal (id) {
assertNonEmpty(id, 'id')
const ok = this._proposals.delete(id)
if (ok) this.emit('expired', { id })
return ok
}
sweepExpired () {
const now = Date.now()
let n = 0
for (const [id, p] of this._proposals) {
if (p.expiresAt && now > p.expiresAt && !p.approved) {
this._proposals.delete(id)
n++
}
}
if (n) this.emit('sweep', { removed: n })
return n
}
_gossip (data) { _gossip (data) {
if (!this._peerMsgs) return if (!this._peerMsgs) return
gossipSend(this, data) gossipSend(this, data)
@@ -36,3 +36,12 @@ test('getStats', async (t) => {
t.is(m.getStats().created, 1) t.is(m.getStats().created, 1)
await m.close() await m.close()
}) })
test('ttl sweep and listProposals', async (t) => {
const m = new HyperP2PMultisigThreshold()
m.createProposal('exp', ['a', 'b'], 2, { expiresAt: Date.now() - 1 })
t.is(m.listProposals().length, 1)
t.is(m.sweepExpired(), 1)
t.is(m.listProposals().length, 0)
await m.close()
})
@@ -102,6 +102,39 @@ class HyperP2PTrustGraph extends EventEmitter {
return nodes.size return nodes.size
} }
listNeighbors (from) {
const f = typeof from === 'string' ? from : b4a.toString(from, 'hex')
const m = this._edges.get(f)
return m ? [...m.keys()] : []
}
removeEdge (from, to) {
const f = typeof from === 'string' ? from : b4a.toString(from, 'hex')
const t = typeof to === 'string' ? to : b4a.toString(to, 'hex')
const m = this._edges.get(f)
if (!m || !m.has(t)) return false
m.delete(t)
if (!m.size) this._edges.delete(f)
this.emit('edge-removed', { from: f, to: t })
return true
}
trustedPeers (from, minScore = 0.5) {
const f = typeof from === 'string' ? from : b4a.toString(from, 'hex')
const candidates = new Set()
for (const edge of this.edges()) {
candidates.add(edge.from)
candidates.add(edge.to)
}
const out = []
for (const peer of candidates) {
if (peer === f) continue
const score = this.trustScore(f, peer)
if (score >= minScore) out.push({ peer, score })
}
return out.sort((a, b) => b.score - a.score)
}
toJSON () { toJSON () {
return { edges: this.edges(), decay: this.decay } return { edges: this.edges(), decay: this.decay }
} }
@@ -10,6 +10,18 @@ test('trust-graph: edge and score', async (t) => {
await g.close() await g.close()
}) })
test('trust-graph: neighbors and trustedPeers', async (t) => {
const g = new HyperP2PTrustGraph()
g.addEdge('a', 'b', 0.9)
t.ok(g.listNeighbors('a').includes('b'))
t.ok(g.removeEdge('a', 'b'))
t.is(g.listNeighbors('a').length, 0)
g.addEdge('a', 'b', 1)
const trusted = g.trustedPeers('a', 0.5)
t.ok(trusted.length >= 1)
await g.close()
})
test('trust-graph: merge', async (t) => { test('trust-graph: merge', async (t) => {
const a = new HyperP2PTrustGraph() const a = new HyperP2PTrustGraph()
const b = new HyperP2PTrustGraph() const b = new HyperP2PTrustGraph()