diff --git a/.claude/memory/alpha-hardening-navigation.md b/.claude/memory/alpha-hardening-navigation.md index 9e9ef77..1fd8237 100644 --- a/.claude/memory/alpha-hardening-navigation.md +++ b/.claude/memory/alpha-hardening-navigation.md @@ -26,7 +26,7 @@ Active workstream (started 2026-07-04): alpha hardening of the money/trust path. Both are already migrated into `.scratch/alpha-hardening/prd.json` (AH-021 updated, AH-023 added) and the README index — ready for Ralph to pick up unattended. -**Ralph note:** `scripts/ralph_progress.py` tracks `docs/prd.json` (35/35 done) and does NOT see `.scratch/alpha-hardening/issues/`. No ralph loop is running and no `.ralph-tui/` state exists. `.scratch/alpha-hardening/prd.json` now has 23 stories (AH-001…AH-023); point Ralph at that file for the alpha-hardening branch. Do NOT use `ralph auto --parallel` on server.py-touching issues — 21 and 23 both touch `server.py`/`billing.py`/`audit.py`; if run in the same Ralph pass, run them serially, not in parallel (merge-conflict risk, same lesson as 03/04 previously). +**Ralph note:** `scripts/ralph_progress.py` tracks `docs/prd.json` (US-001…US-047; base 35/35 done, friends-test arc 36–47 open/in-progress). Alpha hardening uses `.scratch/alpha-hardening/prd.json` (AH-001…AH-023). Point Ralph at the prd.json for the branch you're running. **Why:** three audits agreed the alpha blockers are unauthenticated gossip (anyone can inject billing events), the free-credit faucet, and ephemeral bans. **How to apply:** work test-first per issue acceptance criteria; use `.venv`; `cryptography` belongs in node deps (wallet.py imports it — causes many of the 24 "failures" in a fresh env). See [[project-status]] and [[autonomous-work-style]]. diff --git a/.claude/memory/project-status.md b/.claude/memory/project-status.md index 4c53bad..0c4dd4c 100644 --- a/.claude/memory/project-status.md +++ b/.claude/memory/project-status.md @@ -8,7 +8,7 @@ metadata: # Project Status (2026-07-13) -> Doc reconciliation 2026-07-13: `docs/PRD.md`, ADR numbering (0024 = distributed GGUF), and `docs/issues/` statuses updated to match `docs/prd.json` (US-001…US-035 done). Post-035 work remains in issues 36–48 and `.scratch/`. +> Doc reconciliation 2026-07-13: `docs/PRD.md`, ADR numbering (0024 = distributed GGUF), and `docs/issues/` statuses updated to match `docs/prd.json` (US-001…US-035 done). **US-036…US-047** added to `docs/prd.json` for the friends-test / LAN-serving arc. Post-035 scratch work remains in `.scratch/`. All 35 user stories in docs/prd.json are done (35/35), including the reward-system arc US-030…US-035 completed 2026-07-02: diff --git a/docs/PRD.md b/docs/PRD.md index 9a5ad10..334b97b 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -1,4 +1,4 @@ -Status: done (base program US-001…US-035 complete; see `docs/prd.json`. Post-035 work lives in `docs/issues/36+` and `.scratch/`. Payment/settlement superseded by ADR-0015; fraud by ADR-0018.) +Status: done (base program US-001…US-035 complete; friends-test arc US-036…US-047 in `docs/prd.json`. Payment/settlement superseded by ADR-0015; fraud by ADR-0018.) # Distributed Inference Network — PRD diff --git a/docs/prd.json b/docs/prd.json index 7a9f307..c10a8c2 100644 --- a/docs/prd.json +++ b/docs/prd.json @@ -1,6 +1,6 @@ { "name": "Distributed Inference Network", - "description": "Build a distributed inference network with node, gateway, tracker, SDK, contracts, and P2P shard distribution components from the grill session PRD.", + "description": "Distributed inference network: base program US-001…US-035 complete; post-alpha friends-test and LAN-serving arc US-036…US-047 tracked here. Active scratch features (alpha hardening, NCA, distributed GGUF) have separate prd.json files under .scratch/.", "branchName": "ralph/distributed-inference-network", "userStories": [ { @@ -814,10 +814,247 @@ "US-033" ], "completionNotes": "GET /dashboard served from embedded dashboard.html (package-data, no build step) by any tracker. Panels: hive/leader (raft status), nodes+coverage grouped by model, client balances, node pending + protocol cut, settlement history with devnet explorer links, strikes/bans/forfeitures (GET /v1/registry/wallets + snapshot forfeits), RPM stats. 4s auto-refresh via fetch polling. 3 tests in tests/test_dashboard.py." + }, + { + "id": "US-036", + "title": "36 — Streamed chat completions over the relay RPC path", + "description": "Public NAT deployments proxy every chat request tracker → relay → head node. Implement true multi-frame SSE streaming over the relay WebSocket so clients see live tokens and relayed streams bill through the same SSE accounting loop as direct proxy streams. Inter-node /forward activation hops stay single-frame (ADR-0014).", + "acceptanceCriteria": [ + "stream: true chat via relay delivers SSE chunks incrementally (≥2 distinct frame arrivals before [DONE])", + "Relayed streamed request records nonzero billed tokens and node work credit", + "Non-streamed relayed requests and /forward binary hops behave exactly as before (single frame, body_base64 intact)", + "Legacy single-frame response from an old node is accepted as terminal", + "Idle stream (no frame for 120 s) returns 504 and cleans up the relay-side queue", + "Extend tests/test_gossip_and_relay.py alongside test_relay_rpc_round_trips_http_request_to_peer", + "python -m pytest passes from repo root" + ], + "priority": 36, + "status": "needs-review", + "notes": "Source issue: docs/issues/36-relay-streamed-chat.md. Implemented via _stream_relayed_frames in server.py; verify on public NAT relay before friends-test.", + "dependsOn": [ + "US-029", + "US-031" + ], + "completionNotes": "Multi-frame relay-http-response protocol; node relay_bridge line-by-line SSE emit; relay server per-request asyncio.Queue; tracker _stream_relayed_frames with SSE billing parity. Client mid-stream disconnect accepted limitation for alpha." + }, + { + "id": "US-037", + "title": "37 — Concurrent request handling in the node relay bridge", + "description": "RelayHttpBridge currently handles relay-http-request envelopes serially, blocking up to 300 s per request. Off-LAN a node can be head of one route and downstream hop of another — overlapping routes through a shared node break. Dispatch on a bounded ThreadPoolExecutor (default 8, configurable) with per-frame WS send locking compatible with US-036 streaming.", + "acceptanceCriteria": [ + "While one relayed request is in flight, a second relay-http-request to the same node completes without waiting for the first", + "Responses are correctly matched by request_id when they complete out of order", + "More than N simultaneous requests queue and all eventually complete; thread count never exceeds N workers", + "Bridge survives a relay reconnect with workers still in flight (no crash, no deadlock; orphaned responses dropped)", + "Configurable via meshnet-node start --relay-concurrency N (env MESHNET_RELAY_CONCURRENCY)", + "Extend tests/test_gossip_and_relay.py", + "python -m pytest passes from repo root" + ], + "priority": 37, + "status": "open", + "notes": "Source issue: docs/issues/37-relay-bridge-concurrency.md. Critical for public friends-test; blocks concurrent head + hop on same node.", + "dependsOn": [ + "US-036" + ] + }, + { + "id": "US-038", + "title": "38 — Tracker cluster join via a single seed peer", + "description": "Tracker cluster membership is static today — a newcomer configured with only one existing peer is never learned by the rest of the hive and quorum math diverges. A joining tracker configured with any one live seed announces via hive-HMAC-signed POST /v1/cluster/join; membership changes replicate through the Raft log and persist across restarts.", + "acceptanceCriteria": [ + "Start trackers A+B; start C with only A as seed → within one election timeout A, B, and C report the same 3-peer membership on GET /v1/cluster/peers, and a value proposed on C commits on A and B", + "Join without a valid hive signature is rejected with 403; join to a follower is forwarded to the leader", + "Restarting C with its seed offline rejoins from persisted membership", + "Standalone tracker (no seeds) behaves exactly as today", + "python -m pytest passes from repo root" + ], + "priority": 38, + "status": "open", + "notes": "Source issue: docs/issues/38-tracker-seed-join.md. Out of scope: peer removal, joint consensus, automatic seed retry.", + "dependsOn": [ + "US-013", + "US-017" + ] + }, + { + "id": "US-039", + "title": "39 — Caller Credit granted once per account; chat requires account keys", + "description": "DEFAULT_STARTING_CREDIT=0 and no grant path leaves every fresh public tracker request at 402. Grant Caller Credit once per account on first API key creation via deterministic event id caller-credit-{account_id}; chat on accounts-enabled trackers requires a real active sk-mesh- key (401 for invented bearers).", + "acceptanceCriteria": [ + "Fresh account → first key → key has --starting-credit balance; chat succeeds", + "Second key on the same account → no additional credit", + "Revoke-and-recreate keys → still no additional credit (deterministic event id)", + "Random bearer string on an accounts-enabled tracker → 401, never 402/free work", + "Tracker without accounts store: gate behavior unchanged", + "--starting-credit 0 disables the grant entirely (mainnet posture)", + "python -m pytest passes from repo root" + ], + "priority": 39, + "status": "open", + "notes": "Source issue: docs/issues/39-caller-credit-account-keys.md. Critical for friends-test inference.", + "dependsOn": [ + "US-031", + "US-035" + ] + }, + { + "id": "US-040", + "title": "40 — Devnet top-up button on the dashboard", + "description": "After Caller Credit (US-039) is spent, devnet friends need a dashboard faucet refill without on-chain USDT deposits. POST /v1/account/topup (session-authenticated) credits a configured fixed amount per click; flag off returns 404 and hides the button.", + "acceptanceCriteria": [ + "Flag off: endpoint 404s, dashboard shows no top-up button", + "Flag on: logged-in user tops up own key, balance rises by exactly N", + "Topping up another account's key → 403", + "python -m pytest passes from repo root" + ], + "priority": 40, + "status": "open", + "notes": "Source issue: docs/issues/40-devnet-dashboard-topup.md. Mainnet deployments set --devnet-topup 0.", + "dependsOn": [ + "US-039" + ] + }, + { + "id": "US-041", + "title": "41 — Account wallet: browser-extension signing, in-browser generation, export-only", + "description": "Accounts need a visible wallet for deposit attribution without the tracker ever holding private keys. Dashboard integrates Solana wallet-adapter connect+nonce proof, or in-browser keypair generation with one-time export; no private-key import endpoint.", + "acceptanceCriteria": [ + "Connect-extension flow stores a verified pubkey (rejects unsigned/mismatched nonce proofs)", + "Generate flow: pubkey lands on the account; private key is never sent to the tracker, export works", + "No endpoint or UI accepts a private key", + "Deposits to the shown address credit the account's keys via the existing watcher", + "Address visible on the account panel after either flow", + "python -m pytest passes from repo root" + ], + "priority": 41, + "status": "open", + "notes": "Source issue: docs/issues/41-account-wallet-keypair.md. Not needed for devnet friends test; needed before mainnet.", + "dependsOn": [ + "US-032", + "US-039" + ] + }, + { + "id": "US-042", + "title": "42 — GGUF/llama.cpp node backend (phase C whole-model first)", + "description": "Node backend is transformers-only today; large MoE models on consumer hardware require GGUF via llama.cpp. Phase C: whole-model GGUF nodes (single-hop routes) first; partial-layer distributed GGUF deferred to ADR-0024. Also: GGUF catalog entries, Strix Halo/Vulkan hardware detection, download dir applies to GGUF files.", + "acceptanceCriteria": [ + "A node with --gguf --quant IQ3_XXS serves /v1/chat/completions via llama.cpp with GPU offload where available", + "Tracker treats it as a full-coverage node (single-hop routes, billing works)", + "Streamed responses work through the tracker proxy and the relay (US-036)", + "python -m pytest passes from repo root (llama.cpp behind an optional extra)" + ], + "priority": 42, + "status": "in-design", + "notes": "Source issue: docs/issues/42-gguf-llamacpp-node-backend.md. Distributed native path: ADR-0024. Sequencing: phase C before A/B investigation.", + "dependsOn": [ + "US-036" + ] + }, + { + "id": "US-043", + "title": "43 — Dashboard model search and model cards", + "description": "Dashboard lacks model-centric discovery. Add server-side HF search proxy merged with tracker presets and live coverage; model cards show architecture, coverage gaps, pricing, memory per quant, and a request-this-model action. Featured section driven by CURATED_MODELS including GGUF once US-042 lands.", + "acceptanceCriteria": [ + "Searching a HF repo id or free text returns results without the browser calling HF directly", + "A served model's card shows live coverage and a working chat-now state", + "An unserved model's card shows the request action and estimated memory per quant", + "python -m pytest passes from repo root" + ], + "priority": 43, + "status": "open", + "notes": "Source issue: docs/issues/43-dashboard-model-search-cards.md. Post-deploy polish.", + "dependsOn": [ + "US-035" + ] + }, + { + "id": "US-044", + "title": "44 — Tracker as model-file source; nodes download only their shard", + "description": "Second nodes joining a fleet today download entire HF snapshots even for small shard assignments. Tracker --models-dir advertises layer-scoped safetensors subsets; nodes race tracker/peer sources before HF allow_patterns fallback. Hard half remaining: meta-device partial model materialization so resident memory scales with shard size, not full model size.", + "acceptanceCriteria": [ + "Tracker started with --models-dir / MESHNET_MODELS_DIR advertises local model-file sources in assignment responses", + "Tracker serves a tar stream (or per-file API) containing only safetensors files for the assigned layer range plus config/tokenizer/index metadata", + "Node downloader tries exact-shard peers, then tracker/peer file subsets, then HF snapshot_download with allow_patterns — never silently full-repo when layer index is available", + "Two-machine test: machine B receives only its assigned range from machine A — nothing fetched from HF", + "Machine B resident memory scales with its shard size, not model size", + "Checksums verified end-to-end; corrupted transfer falls back cleanly", + "Single-node/full-model flows unchanged", + "python -m pytest passes from repo root" + ], + "priority": 44, + "status": "in-progress", + "notes": "Source issue: docs/issues/44-tracker-shard-source-partial-download.md. Download path largely implemented 2026-07-06; partial LOAD (meta-device materialization) and two-machine acceptance remain.", + "dependsOn": [ + "US-004", + "US-012" + ], + "completionNotes": "Tracker models-dir indexing, layer-scoped tar stream, HF allow_patterns client-side from remote index, per-file download API with retries, symlink dereference in tar writers. Remaining: true partial model load and live two-machine verification." + }, + { + "id": "US-045", + "title": "45 — Dual-rate billing: separate input and output token prices", + "description": "Ledger has one price_per_1k_tokens and stream vs non-stream paths disagree on input vs output counting. Charge both input and output tokens at separate rates per model; HF pricing refresher applies 80% of each marketplace side separately.", + "acceptanceCriteria": [ + "Streamed and non-streamed requests for the same exchange bill the same split (input charged in both)", + "A model with asymmetric provider rates bills input and output differently; usage_for / billing events expose the split", + "Old persisted billing events replay byte-identically (balances unchanged)", + "HF refresh sets both rates from the marketplace row, not the average", + "Spend cap (--max-charge-per-request) uses the dual rates", + "python -m pytest passes from repo root" + ], + "priority": 45, + "status": "in-progress", + "notes": "Source issue: docs/issues/45-dual-rate-billing.md. Billing correctness before friends test.", + "dependsOn": [ + "US-031" + ] + }, + { + "id": "US-046", + "title": "46 — Tracker .env awareness + first-node auto-join bootstrap", + "description": "Fresh trackers return 503 on auto-join because deployability ignores the joining caller's hardware, and meshnet-tracker ignores .env MESHNET_DOWNLOAD_DIR. Fix empty-registry bootstrap, tracker env loading parity with node CLI, models-dir fallback chain, and tar dereference for HF symlink snapshots.", + "acceptanceCriteria": [ + "Fresh tracker (empty registry) + caller with enough memory for a recommended preset → /v1/network/assign returns 200 with model_sources populated when tracker holds a local snapshot", + "Fresh tracker + caller too small for any recommended preset → still 503", + "meshnet-tracker start in a directory with .env setting MESHNET_DOWNLOAD_DIR serves /v1/model-files/download from that dir with no extra flags", + "Explicit --models-dir and MESHNET_MODELS_DIR still take precedence", + "python -m pytest passes from repo root" + ], + "priority": 46, + "status": "needs-review", + "notes": "Source issue: docs/issues/46-tracker-env-and-first-node-autojoin.md. Verified live 2026-07-06.", + "dependsOn": [ + "US-044" + ], + "completionNotes": "Empty-registry synthesizes caller as candidate node; tracker loads .env; models_dir falls back MESHNET_MODELS_DIR → MESHNET_DOWNLOAD_DIR; tar dereference=True. Pytest passes aside from known port-7000 env conflict." + }, + { + "id": "US-047", + "title": "47 — Tracker-first model downloads: visibility, sane timeouts, RAM-based sizing", + "description": "Explicit --model startup should skip pointless auto-join; tracker/peer sources preferred over HF with visible progress and 30 s socket timeouts; client abort during tar stream logs one line; CPU nodes size shards from RAM not phantom GPU VRAM; per-file downloads for robustness over fragile multi-GB tar streams.", + "acceptanceCriteria": [ + "Node started with explicit --model never queries /v1/network/assign and never prints auto-join unavailable", + "Tracker/peer model source preferred; HF only when no source, all sources fail, or --tracker-source-disabled", + "Tracker-source downloads print progress every 512 MB and print exception + URL on failure", + "A ≥2 s read stall no longer aborts a tracker model-source download (30 s socket timeout)", + "Client disconnect during /v1/model-files/download logs one line on the tracker, no traceback", + "CPU node with big RAM gets a RAM-sized shard: sizing paths ignore VRAM unless device=cuda", + "Live two-machine retest: Windows node downloads from tracker at LAN speed with RAM-sized shard assignment", + "python -m pytest passes from repo root" + ], + "priority": 47, + "status": "in-progress", + "notes": "Source issue: docs/issues/47-model-source-download-visibility.md. Engineering largely complete 2026-07-06; live two-machine retest pending.", + "dependsOn": [ + "US-044", + "US-046" + ], + "completionNotes": "Skip auto-join when model explicit; sequential source try with progress; 30 s model-source timeout; assignment_vram_mb for CPU; per-file /v1/model-files/download with manifest and retries. Remaining: live Windows two-machine retest." } ], "metadata": { - "updatedAt": "2026-07-01T00:00:00.000Z", + "updatedAt": "2026-07-13T16:15:00.000Z", "statusVocabulary": { "open": "Not started", "in-design": "Decisions pending before implementation can begin",