Skip to content

feat: Gensyn AXL mesh + 0G memory pane#58

Merged
WhyAsh5114 merged 46 commits into
mainfrom
feat/axl-mesh
May 3, 2026
Merged

feat: Gensyn AXL mesh + 0G memory pane#58
WhyAsh5114 merged 46 commits into
mainfrom
feat/axl-mesh

Conversation

@WhyAsh5114
Copy link
Copy Markdown
Owner

@WhyAsh5114 WhyAsh5114 commented May 2, 2026

Closes #50
Closes #48

What's in this PR

Gensyn AXL P2P mesh (mcp-mesh)

Adds a full MCP server that manages an AXL node subprocess inside each workspace container, enabling peer-to-peer collaboration between agent instances.

packages/mcp-mesh

  • node-manager.ts — spawns and health-checks the AXL binary; exposes structured peer list
  • axl-client.ts — typed HTTP client for the AXL local API bridge
  • server.ts + index.ts — MCP server on port 3105 with 5 agent-callable tools:
    • list_peers — live peer list from the AXL node
    • broadcast_help — sends a structured help request (revert sig + trace + contract source) to peers; returns a requestId
    • collect_responses — polls for peer patch suggestions up to a TTL; empty array on timeout (not an error)
    • respond — answers a peer's help request with a locally verified patch
    • verify_peer_patch — applies a candidate patch against a Hardhat chain snapshot and returns pass/fail

Runtime integration

  • packages/backend/runtime/Dockerfile + entrypoint.sh — mcp-mesh added to the container with a restart loop on port 3105
  • packages/backend/src/lib/runtime-docker.ts — port 3105 (mesh) wired into getWorkspaceContainerPorts
  • packages/backend/src/lib/tool-exec.tsmesh.* tool namespace proxied end-to-end
  • packages/agent/src/system-prompt.ts — Workflow D documented: "use mesh.broadcast_help only when memory.recall returns no results"
  • packages/frontend/src/lib/components/events/event-row.sveltemesh_help_broadcast and mesh_help_received event rows added to the chat stream

0G KV memory purge API (mcp-memory + backend)

packages/mcp-memory

  • service.tspurge method added to MemoryService interface; FS backend filters and rewrites patterns.json; KV backend submits tombstone (empty-byte) entries via Batcher for each pattern key
  • index.tsDELETE /patterns route added; memoryToolForPath updated to route DELETE /patternspurge

packages/backend/src/api/workspace.ts

  • DELETE /workspace/{id}/memory — purge proxy (previously wired but handler was missing)
  • GET /workspace/{id}/memory/patterns — lists patterns for a scope, or fetches both scopes in parallel when no filter is provided
  • GET /workspace/{id}/memory/embed — batch-embeds all patterns via the configured OpenAI-compatible endpoint (OPENAI_BASE_URL + OPENAI_API_KEY); returns 503/runtime_unavailable when env vars are absent so the UI can degrade gracefully

Memory pane UI (frontend)

A new fourth tab in the workspace layout alongside editor / preview / wallet.

packages/frontend/src/lib/components/panes/memory-pane.svelte

  • Scope filter bar (all / local / mesh) with live counts and inline purge confirm
  • List view — pattern cards with scope badge (indigo = local, amber = mesh), revertSignature, coloured unified-diff preview, provenance line
  • Graph view — pure-SVG force simulation (no d3), edges derived from cosine similarity ≥ 0.72 when embeddings are available, falling back to shared revertSignature grouping; hover tooltip; selected-node detail side panel
  • Graph status bar shows embedding availability or fallback notice (set OPENAI_BASE_URL to enable semantic edges)

packages/frontend/src/lib/api/workspace.tslistMemoryPatterns, embedMemoryPatterns, purgeMemory added to WorkspaceClient

packages/frontend/src/routes/workspaces/[id]/+page.svelte — memory wired as the 4th Tabs.Trigger / Tabs.Content


Path-based preview proxy + production deployment fixes

packages/backend/src/api/preview.ts (new file)

  • GET /preview/:workspaceId/* — HTTP proxy that forwards requests to the per-workspace Vite dev server running on a loopback port; Vite is started with base=/preview/<id>/ so all asset URLs include the prefix
  • WebSocket proxy for Vite HMR — upgradeWebSocket handler on the same path routes ws:// connections to the internal Vite WS port; HMR works through the public domain without extra ports

packages/backend/src/lib/preview-manager.ts — when CRUCIBLE_APP_URL is set, previewUrl is ${APP_URL}/preview/${workspaceId} and Vite base is set accordingly; subdomain model deferred

packages/backend/src/index.ts — backend binds to process.env['HOST'] ?? '127.0.0.1'; set HOST=0.0.0.0 in production so nginx can proxy

packages/backend/src/lib/runtime-docker.tsExtraHosts: ['host.docker.internal:host-gateway'] added to HostConfig; Linux Docker does not add this automatically so containers could not reach the backend on EC2


Bug fixes

  • packages/types/src/mcp/memory.tslimit changed from z.number() to z.coerce.number() so HTTP query-string values pass Zod validation
  • packages/mcp-memory/src/service.tsStoredPatternSchema.safeParse() on every KV entry skips foreign mesh objects; write-through writeCache map ensures newly stored patterns are readable immediately before the 0G indexer propagates; workspace-specific KV stream ID derived as keccak256(signerAddress + ':' + workspaceId) to avoid cross-workspace key collisions
  • packages/agent/src/loop.tsReadableStream type-cast fixes; fetch URL references updated for preview proxy
  • scripts/seed-memories.sh — fully sequential writes to avoid 0G KV version conflicts from concurrent Date.now() batchers

Testing

# type-check
cd packages/frontend && bun run check-types   # 0 errors

# hit the memory service directly
curl "http://127.0.0.1:<port>/patterns?scope=local&limit=200"
curl "http://127.0.0.1:<port>/patterns?scope=mesh&limit=200"

# preview proxy (with CRUCIBLE_APP_URL set)
curl "http://localhost:3000/preview/<workspaceId>/"

WhyAsh5114 and others added 30 commits May 3, 2026 01:08
Implements a new mcp-mesh package exposing five tools over HTTP:
- list_peers
- broadcast_help
- collect_responses
- respond
- verify_peer_patch

Uses the Gensyn AXL p2p network to let agents collaborate on
contract repair patches across workspaces.
- Add Go builder stage to compile axl-node from gensyn-ai/axl
- Copy axl-node binary into final image at /usr/local/bin/axl-node
- Copy mcp-mesh package into the image
- Start mcp-mesh via the supervise helper on MESH_MCP_PORT (3105)
- Include mesh_pid in shutdown/wait to ensure clean teardown
- Expose port 3105 and set MESH_MCP_PORT env default
- Add CONTAINER_MESH_PORT (3105) constant in runtime-docker
- Extend WorkspaceRuntimePorts with mesh field
- Pass MESH_MCP_PORT env var and expose/bind the container port
- Extract mesh host port in ensureWorkspaceContainer and
  getWorkspaceContainerPorts
- Populate MESH_ROUTES with the five mesh tool endpoints
- Route tool-exec mesh calls to ports.mesh instead of null
Documents the mesh MCP server in the available servers list and
adds Workflow D — triggered when memory.recall returns no hits and
LLM reasoning alone is insufficient:

  Step 1  mesh.broadcast_help (revertSig, trace, ctx, ttlMs)
  Step 2  mesh.collect_responses (waitMs: 15 s)
  Step 3  mesh.verify_peer_patch per response (accept only 'verified')
  Step 4  apply patch, compile, revert chain, redeploy
  Step 5  memory.remember with source: 'mesh'

Includes fallback to LLM reasoning when no peers respond, and
safety rules (never skip verify_peer_patch, never broadcast secrets).
Adds two new Svelte components for displaying mesh events in the chat:
- MeshHelpBroadcastRow — shown when the agent broadcasts a help request
- MeshHelpReceivedRow  — shown when a peer response arrives

Registers both in event-row.svelte for the mesh_help_broadcast and
mesh_help_received event types.
- Register @crucible/mcp-mesh in the root workspaces array
- Limit `turbo run dev` to backend + frontend only (mcp-mesh and other
  MCP servers do not have a dev script and would cause turbo warnings)
- Update bun.lock for the new package
- Add PurgeInput/PurgeOutput schemas and purge entry to tools map in types
- Replace StoredPattern interface with a Zod schema for runtime validation;
  readPatternsFs now silently discards malformed records
- Default listPatterns scope to 'local' when no scope is provided
- Implement purge(input) in both FS and KV service backends:
    FS  — filters out patterns for the given scope (or all) then rewrites
    KV  — writes empty Uint8Array tombstones via Batcher for each pattern
- Add DELETE /patterns route (purgeRoute) with query-param scope filter
- Fix memoryToolForPath to check method so DELETE /patterns resolves to
  'purge' instead of 'list_patterns'
Exposes a purgeMemoryRoute that proxies to the mcp-memory service:
- Validates ownership via prisma before forwarding
- Returns 503 if the memory service port is not yet available
- Forwards optional scope query param (local | mesh) to mcp-memory
- Returns { deleted: number } matching the PurgeOutput schema
HTTP query params arrive as strings; z.coerce.number() converts
"200" → 200 so GET /patterns?limit=200 no longer fails Zod validation.
The mesh KV stream is shared with mcp-mesh which writes node role
objects to it. readAllPatterns was casting every entry as StoredPattern
without validation, letting foreign objects leak through as patterns.
Now uses StoredPatternSchema.safeParse() and skips any non-conforming
entry, consistent with how the FS backend works in readPatternsFs.
- GET  /workspace/{id}/memory/patterns — lists patterns for a scope (or
  both local+mesh in parallel when no scope filter is given)
- GET  /workspace/{id}/memory/embed — batch-embeds all patterns via the
  OpenAI-compatible endpoint (OPENAI_BASE_URL + OPENAI_API_KEY); returns
  503/runtime_unavailable when env vars are absent
- DELETE /workspace/{id}/memory — purge endpoint (previously missing handler)

All error paths use 503 to match the declared OpenAPI response schema.
WorkspaceClient gains three new methods:
  listMemoryPatterns, embedMemoryPatterns, purgeMemory

memory-pane.svelte provides:
- Scope filter bar (all / local / mesh) with inline purge confirm
- List view: pattern cards with scope badge, revertSignature, coloured
  diff preview, and provenance
- Graph view: pure-SVG force simulation (no d3), indigo nodes for local
  scope, amber for mesh; edges from cosine similarity >= 0.72 when
  embeddings are available, or shared revertSignature as fallback
- Hover tooltip and selected-node detail panel

+page.svelte wires memory as a 4th main tab alongside editor / preview /
wallet, importing BrainIcon from lucide-svelte.
- packages/mcp-mesh/test/server.test.ts: factory smoke test (createMeshServer
  constructs without error, exposes connect())
- packages/mcp-mesh/test/node-manager.test.ts: unit tests for verifyPeerPatch
  (bad patch, bad receipt, valid path) and collectResponses (empty queue
  returns [] immediately with waitMs=0)
- packages/mcp-memory/test/service.test.ts: two new cases for purge —
  scoped delete preserves the other scope; unscoped delete clears all patterns
- fix(mcp-mesh): remove unused ListPeersInputSchema and readFile imports
  that were causing eslint no-unused-vars errors in CI
…r URLs

McpServerKey type and getMcpSchemas() switch in loop.ts both omitted 'mesh',
so the mesh MCP server was never registered with the AI SDK and list_peers /
broadcast_help / collect_responses / respond / verify_peer_patch were invisible
to the agent.

inference.ts likewise omitted 'mesh' from the mcpServerUrls Record type and
loop, so the mesh service URL was never passed to runAgentTurn even when the
container's mesh port was live.
…p peers

list_peers was returning the 2 Gensyn bootstrap relay nodes (34.46.48.224
and 136.111.135.206) as 'peers' — they are infrastructure, not Crucible
workspaces. topology.peers only contains direct TLS connections, which are
always the bootstraps for leaf nodes behind NAT.

Fix: parse the 'tree' field from AXL /topology (the spanning-tree gossip
table) and filter out:
- own public key
- direct bootstrap keys (topology.peers)
- root/relay nodes (parent === self)

The remaining entries are actual remote workspace nodes. broadcastHelp is
updated to target the same filtered tree entries instead of bootstraps.

Note: as of current AXL behaviour, sibling leaf nodes don't yet propagate
their tree entries to each other (tree gossip stays local), so list_peers
will return [] until AXL fixes cross-leaf tree propagation. This is honest
rather than showing fake bootstrap 'peers'.

Also: compact prose table styling in Markdown.svelte (text-xs on th/td).
Co-authored-by: Copilot <copilot@github.com>
- Add axlPublicKey and meshPort fields to WorkspaceRuntime schema
  and Prisma migration 20260502210741_add_axl_public_key_and_mesh_port
- Add POST /api/workspace/:id/axl-key (container auth) — mcp-mesh
  calls this on startup to register its AXL public key
- Add GET /api/workspace/:id/mesh-peers (container auth) — returns
  AXL public keys for all other workspaces owned by the same user
- Export containerApi from workspace.ts and mount at /api in index.ts
- Auth via X-Container-Secret header (CRUCIBLE_RUNTIME_SECRET env var
  shared between host backend and containers)
- Forward CRUCIBLE_BACKEND_URL and CRUCIBLE_RUNTIME_SECRET into
  container env in buildContainerEnv (runtime-docker.ts)
- AXLNodeManager now accepts AXLNodeManagerConfig (backendUrl,
  workspaceId, runtimeSecret) and exposes registerOwnKey()
- listPeers() and broadcastHelp() now merge spanning-tree entries
  with backend-registered peers (fixes NAT peer-discovery gap)
- New Zod schemas in @crucible/types: AxlKeyRegisterRequest/Response,
  MeshPeerEntry, MeshPeersResponse
Drop scope from RememberInput — callers no longer tag patterns as local/mesh
manually. Instead, an optional fromPeerId field carries the AXL public key of
the peer that supplied the patch (Workflow D). The service derives:

  scope        = fromPeerId ? 'mesh' : 'local'
  authorNode   = fromPeerId ?? ownNodeId

This fixes a latent bug where peer-sourced patterns were attributed to the
receiving node in provenance.authorNode, losing the actual author.
Replace scope: 'local'/'mesh' with fromPeerId in all service.remember() test
calls. Mesh patterns now pass a fake peer key; local patterns omit the field.

Also update Workflow D Step 5 in the agent system prompt: the agent now passes
fromPeerId: response.peerId instead of scope: 'mesh', and updates the
ARCHITECTURE.md remember() signature accordingly.
…erns

Declare localRes/meshRes without null initialisation so eslint's
no-useless-assignment rule is satisfied — the destructured assignment
from Promise.all is the only write.
Discovers all running crucible-ws-* containers and injects realistic
DeFi memory patterns spread across them so the memory pane shows a live
mesh: each node has its own local patterns plus cross-mesh patterns
received from the other node (via fromPeerId).

Patterns cover: Uniswap V2 (STF, EXPIRED, K invariant), ERC20 allowance,
OTC fill/replay, Compound liquidity, and Safe multisig threshold.

Usage:
  bash scripts/seed-memories.sh            # auto-discover
  bash scripts/seed-memories.sh ws-a ws-b  # explicit containers
…i mount order

Generate an ephemeral runtime secret at startup when none is provided so
in-container services (mcp-mesh) can authenticate back to the backend without
manual env-var setup in development.

Also fix the Hono sub-app mount order: containerApi must be registered before
workspaceApi so its container-secret-protected routes (/axl-key, /mesh-peers)
are matched first, before workspaceApi's wildcard requireSession middleware
intercepts them and returns 401.
The memory service starts after the rest of the container stack, so the
first few fetches after a workspace opens legitimately return 503. Return
an empty array instead of throwing so the pane shows a blank state that
self-resolves on the next poll, rather than a toast error on every boot.
Add a search input at the top of the model dropdown that filters the 0G
model and both recommended/others OpenAI lists by name or short label.
Clears on close. Also fixes a Tailwind lint warning (max-w-[96px] →
max-w-24).
After firing all remember writes, poll GET /patterns on each container
every 3s until every seeded pattern ID appears in the response (or
60s timeout). This is necessary because 0G KV writes go through a
batcher.exec() on-chain transaction but the read path queries the KV
indexer, which has a propagation delay of tens of seconds.

On timeout, the script clarifies that patterns were written (transaction
confirmed) and will be visible shortly — rather than leaving the user
wondering whether the seed worked.
macOS ships bash 3.2 which lacks:
- mapfile (replaced with while/read loop)
- declare -A associative arrays (replaced with IDS_A / IDS_B string vars)
- (( x++ )) in set -e context (replaced with x=$((x + 1)))

Also fix verify_container to accept the IDs as a parameter instead of
reading from an associative array, and update caller sites.
The pane previously required producers to write patterns twice — once to
their own local stream and once into the mesh stream of every peer. Drop
that and aggregate server-side: a workspace's mesh view is now the union
of every sibling workspace's locals (re-tagged scope:'mesh') plus any
patterns explicitly pushed into this workspace's own mesh stream.

Also simplify the demo seed script: write only locals (the backend does
the rest), drop the flaky verify-poll loop, and use stdin piping for curl
to avoid shell-quoting issues with patches that contain single quotes.
WhyAsh5114 and others added 16 commits May 3, 2026 12:39
All containers share the same OG_STORAGE_PRIVATE_KEY which derives the
same 0G KV stream ID, so fetching locals from N workspaces returns the
same physical patterns N times. Deduplicate by pattern ID before
returning, with local-scope patterns taking priority over mesh.
Replace the two sequential listMemoryPatterns(local) + listMemoryPatterns(mesh)
calls with a single no-scope call. The backend already returns the combined
deduplicated set when scope is omitted, so there is no longer any chance of
the same pattern ID appearing twice in the frontend array.
All containers share OG_STORAGE_PRIVATE_KEY which previously caused them
all to derive the same localStreamId (zeroPadValue(signerAddress, 32)),
making every workspace read from the same 0G KV stream. Local patterns
from workspace A were therefore indistinguishable from workspace B's.

Now buildContainerEnv derives a unique stream ID per workspace:
  keccak256(signerAddress + ':' + workspaceId) -> 32-byte stream ID

The mesh stream remains shared (all workspaces can read it), while each
workspace's local writes go to its own stream. The backend's cross-
workspace aggregation now correctly surfaces sibling locals as mesh.
Each 0G KV write is an independent on-chain tx taking 30-90 s.
Fire all 8 as background jobs and wait for them all, reducing total
seeding time from ~12 min sequential to ~90 s (single longest write).
Concurrent writes to the same 0G KV stream all get the same Date.now()
batcher version. The KV node requires strictly increasing versions and
silently drops duplicates, causing all-but-one writes to timeout.

Fix: each container's 4 writes run sequentially in a subshell. The two
subshells (A and B) run concurrently. Effective speedup: ~2x.
The 0G KV indexer has propagation delay — writes succeed on-chain but
the iterator returns empty until the block is indexed by the remote KV
node (13.233.48.201). This caused the memory pane to show zero patterns
immediately after seeding.

Add an in-memory Map (writeCache) inside createKvService. On remember(),
cache the StoredPattern before the async on-chain write. readAllPatterns()
merges KV iterator results with any cache entries not yet indexed (by ID).
findById() checks cache first. purge() clears cache entries for purged
patterns.

E2E verified: write returns id, immediate read returns the pattern from
cache, on-chain propagation happens in the background.
Two bugs caused mesh patterns to show as isolated nodes:

1. deriveEdges() skipped pairs where either pattern lacked an embedding
   vector (mesh patterns had no vectors since they are aggregated from
   peers, not stored in the container's own KV scope=mesh stream).
   Fix: fall back to exact revertSignature matching when one or both
   vectors are missing, instead of skipping the pair entirely.

2. embedMemoryRoute fetched scope=mesh from the workspace's OWN
   container, which is always empty in the aggregation model (mesh
   patterns are re-tagged locals from peer containers). Fix: fetch
   locals from each peer workspace container directly, same as
   listMemoryPatternsRoute, so all patterns get cosine-similarity
   embedding vectors.
… clarity

Co-authored-by: Copilot <copilot@github.com>
index.ts:
- Read HOST env var (default 127.0.0.1) and pass as hostname to Bun.serve
  so the server only listens on the configured interface in production.

runtime-docker.ts:
- Add ExtraHosts: ['host.docker.internal:host-gateway'] to every workspace
  container's HostConfig so containers can reach the backend at
  http://host.docker.internal:3000 on Linux (Docker does not inject this
  mapping automatically unlike Docker Desktop on macOS/Windows).
…h URL in workspace file handling

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Copy link
Copy Markdown
Collaborator

@sundaram123krishnan sundaram123krishnan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@WhyAsh5114 WhyAsh5114 merged commit 6a4b78d into main May 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Gensyn] mcp-mesh — AXL node lifecycle + 5 agent tools [Infra] AWS single-host deployment for live demo

2 participants