feat: Gensyn AXL mesh + 0G memory pane#58
Merged
Merged
Conversation
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.
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).
Co-authored-by: Copilot <copilot@github.com>
…h URL in workspace file handling Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-meshnode-manager.ts— spawns and health-checks the AXL binary; exposes structured peer listaxl-client.ts— typed HTTP client for the AXL local API bridgeserver.ts+index.ts— MCP server on port 3105 with 5 agent-callable tools:list_peers— live peer list from the AXL nodebroadcast_help— sends a structured help request (revert sig + trace + contract source) to peers; returns arequestIdcollect_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 patchverify_peer_patch— applies a candidate patch against a Hardhat chain snapshot and returns pass/failRuntime integration
packages/backend/runtime/Dockerfile+entrypoint.sh— mcp-mesh added to the container with a restart loop on port 3105packages/backend/src/lib/runtime-docker.ts— port 3105 (mesh) wired intogetWorkspaceContainerPortspackages/backend/src/lib/tool-exec.ts—mesh.*tool namespace proxied end-to-endpackages/agent/src/system-prompt.ts— Workflow D documented: "usemesh.broadcast_helponly whenmemory.recallreturns no results"packages/frontend/src/lib/components/events/event-row.svelte—mesh_help_broadcastandmesh_help_receivedevent rows added to the chat stream0G KV memory purge API (
mcp-memory+ backend)packages/mcp-memoryservice.ts—purgemethod added toMemoryServiceinterface; FS backend filters and rewritespatterns.json; KV backend submits tombstone (empty-byte) entries viaBatcherfor each pattern keyindex.ts—DELETE /patternsroute added;memoryToolForPathupdated to routeDELETE /patterns→purgepackages/backend/src/api/workspace.tsDELETE /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 providedGET /workspace/{id}/memory/embed— batch-embeds all patterns via the configured OpenAI-compatible endpoint (OPENAI_BASE_URL+OPENAI_API_KEY); returns503/runtime_unavailablewhen env vars are absent so the UI can degrade gracefullyMemory pane UI (
frontend)A new fourth tab in the workspace layout alongside editor / preview / wallet.
packages/frontend/src/lib/components/panes/memory-pane.svelteall / local / mesh) with live counts and inline purge confirmrevertSignature, coloured unified-diff preview, provenance linerevertSignaturegrouping; hover tooltip; selected-node detail side panelset OPENAI_BASE_URL to enable semantic edges)packages/frontend/src/lib/api/workspace.ts—listMemoryPatterns,embedMemoryPatterns,purgeMemoryadded toWorkspaceClientpackages/frontend/src/routes/workspaces/[id]/+page.svelte— memory wired as the 4thTabs.Trigger/Tabs.ContentPath-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 withbase=/preview/<id>/so all asset URLs include the prefixupgradeWebSockethandler on the same path routesws://connections to the internal Vite WS port; HMR works through the public domain without extra portspackages/backend/src/lib/preview-manager.ts— whenCRUCIBLE_APP_URLis set,previewUrlis${APP_URL}/preview/${workspaceId}and Vite base is set accordingly; subdomain model deferredpackages/backend/src/index.ts— backend binds toprocess.env['HOST'] ?? '127.0.0.1'; setHOST=0.0.0.0in production so nginx can proxypackages/backend/src/lib/runtime-docker.ts—ExtraHosts: ['host.docker.internal:host-gateway']added toHostConfig; Linux Docker does not add this automatically so containers could not reach the backend on EC2Bug fixes
packages/types/src/mcp/memory.ts—limitchanged fromz.number()toz.coerce.number()so HTTP query-string values pass Zod validationpackages/mcp-memory/src/service.ts—StoredPatternSchema.safeParse()on every KV entry skips foreign mesh objects; write-throughwriteCachemap ensures newly stored patterns are readable immediately before the 0G indexer propagates; workspace-specific KV stream ID derived askeccak256(signerAddress + ':' + workspaceId)to avoid cross-workspace key collisionspackages/agent/src/loop.ts—ReadableStreamtype-cast fixes;fetchURL references updated for preview proxyscripts/seed-memories.sh— fully sequential writes to avoid 0G KV version conflicts from concurrentDate.now()batchersTesting