Skip to content

Chat backend: read-only conversational grounding over the 11 existing loopover_miner_* tools #6517

Description

@JSONbored

Context

The miner dashboard redesign adds a persistent chat rail (mounted once in __root.tsx, per the chat-rail shell issue) that answers natural-language questions about the miner's own local state. This issue is the backend half only: a new local streaming API endpoint that grounds answers by tool-calling against the miner's existing read-only MCP tools. No UI component, no action-dispatch, no new tool.

The exact 11 tools this endpoint may call — all registered read-only in packages/loopover-miner/bin/loopover-miner-mcp.js (verified by tool name in that file, one server.registerTool(...) call each):

  1. loopover_miner_ping
  2. loopover_miner_get_portfolio_dashboard
  3. loopover_miner_get_manage_status
  4. loopover_miner_list_claims
  5. loopover_miner_get_audit_feed
  6. loopover_miner_get_run_state
  7. loopover_miner_list_plans
  8. loopover_miner_get_plan
  9. loopover_miner_get_governor_decisions
  10. loopover_miner_status
  11. loopover_miner_get_calibration_report

Every one of these tools' own docstring in that file says "Read-only... never mutates" — this was a closed design decision (v1's grounding tool surface is exactly these 11, no new tool surface), not something this issue re-opens.

The existing local API middleware convention this endpoint must follow: apps/loopover-miner-ui/vite-run-state-api.ts (and its siblings vite-portfolio-queue-api.ts, vite-portfolio-queue-actions-api.ts, vite-ledgers-api.ts, vite-governor-api.ts, vite-ranked-candidates-api.ts) are each a small Vite Plugin exposing one /api/* route, registered in apps/loopover-miner-ui/vite.config.ts's plugins array. All of them sit behind apps/loopover-miner-ui/vite-auth.ts, a per-process random-token HttpOnly/SameSite=Strict cookie gate registered FIRST in that array specifically so — per that file's own header comment — "any FUTURE /api/* endpoint... is covered automatically, with no per-endpoint auth wiring required."

Credential/provider resolution to reuse, not reinvent: packages/loopover-engine/src/miner/driver-factory.ts already resolves which local coding-agent provider is configured, from the MINER_CODING_AGENT_PROVIDER env var (comma-separated fallback list) via resolveFirstConfiguredCodingAgentDriverName/isConfiguredCodingAgentDriver/resolveConfiguredCodingAgentDriverNames, with per-provider model/timeout env keys in CODING_AGENT_DRIVER_CONFIG_ENV (MINER_CODING_AGENT_CLAUDE_MODEL, MINER_CODING_AGENT_CODEX_MODEL, MINER_CODING_AGENT_TIMEOUT_MS). All local, no API-key requirement, fail-closed on unknown/unconfigured names. The sibling module packages/loopover-engine/src/miner/agent-sdk-driver.ts already shows the concrete pattern for driving @anthropic-ai/claude-agent-sdk's query() async-iterable in-process (its defaultQuery, dynamically importing @anthropic-ai/claude-agent-sdk — already a packages/loopover-engine dependency per its package.json), including the injected-AgentSdkQueryFn testability seam this issue should mirror so tests never make a real model call.

The existing privacy-boundary precedent to reuse, not reinvent: packages/loopover-engine/src/track-record-summary.ts defines PUBLIC_FIELD_BLOCKLIST, a regex list (trust score, trustscore, reward, payout, ranking, wallet, hotkey, coldkey, etc.) used to keep sensitive fields out of a public-facing summary. loopover_miner_get_governor_decisions's own docstring similarly states it "INTENTIONALLY EXCLUDES the internal/sensitive payload column (reputation / self-plagiarism / budget state) by construction." None of the 11 tools above emit wallet/hotkey/coldkey/reward/trust-score data today (verified by grep across loopover-miner-mcp.js) — but a conversational endpoint adds a NEW leak surface the tools themselves don't have: a user can simply ask "what's my trust score" in plain language, and an ungrounded model could hallucinate an answer instead of correctly having nothing to say.

⚠️ Read this before starting

  • The deliverable is a streaming HTTP endpoint. A PR that returns one buffered JSON response (even if correct) does not satisfy this issue — see the exact wire format required below.
  • The deliverable calls only the 11 loopover_miner_* tools named above. A PR that adds a 12th tool, edits packages/loopover-miner/bin/loopover-miner-mcp.js's registered tool set, or invokes any write-capable loopover_* tool (e.g. loopover_open_pr, loopover_file_issue, anything from src/mcp/local-write-tools.ts / packages/loopover-engine/src/miner/local-write-tools.ts) does NOT resolve this issue and will be closed.
  • This issue is backend only. No chat UI, composer, message-list, or streaming-renderer component, and no change to apps/loopover-miner-ui/src/routes/__root.tsx, belongs in this PR — those are separate sibling issues that will call the endpoint this issue builds.
  • No action-dispatch of any kind (discover/attempt, portfolio release/requeue, governor pause/resume) belongs in this PR, even though those are decided for a later, separately-scoped action-dispatch chat issue. This issue's endpoint must never call /api/portfolio-queue/*, /api/governor/*, or any new discover/attempt-mirroring route.
  • Do not add a new SQLite-backed conversation-history store (mirroring packages/loopover-miner/lib/local-store.js's pattern). The endpoint is stateless: the caller supplies full message history in each request body.

Requirements

  • New endpoint: POST /api/chat, implemented as a new Vite middleware plugin apps/loopover-miner-ui/vite-chat-api.ts (mirroring the handleXRequest-factored-out-for-testing shape already used by vite-run-state-api.ts), registered in apps/loopover-miner-ui/vite.config.ts's plugins array after authPlugin() — the same position every existing /api/* plugin already uses, so the auth-cookie gate covers it with no new auth mechanism.
  • Request body: { "messages": [{ "role": "user" | "assistant", "content": string }, ...] }; the endpoint must reject (4xx, non-streamed JSON error body) a request whose last message is not role: "user" or whose messages array is empty/malformed.
  • Response transport: Content-Type: text/event-stream, chunked, consumed via fetch() + ReadableStream on the client (not the native EventSource API, since EventSource cannot send a POST body) — each event is one data: <json>\n\n line. Valid event types: "text" ({ "type": "text", "text": string }, a partial or full answer chunk), "tool_call" ({ "type": "tool_call", "tool": string, "input": object }), "tool_result" ({ "type": "tool_result", "tool": string, "output": unknown }), "error" ({ "type": "error", "code": string, "message": string }), and a final "done" ({ "type": "done" }) that always terminates the stream, including on error.
  • New engine-side module packages/loopover-engine/src/miner/chat-grounding.ts, exported from packages/loopover-engine/src/index.ts alongside the existing driver-factory.js/agent-sdk-driver.js exports (matching that barrel file's existing convention). It resolves the configured provider using driver-factory.ts's exported functions (resolveFirstConfiguredCodingAgentDriverName, isConfiguredCodingAgentDriver, CODING_AGENT_DRIVER_CONFIG_ENV) — do not read MINER_CODING_AGENT_* env vars directly or duplicate that parsing logic.
  • Chat requires the agent-sdk provider specifically. driver-factory.ts's claude-cli/codex-cli drivers are a task-shaped, single-turn, buffered CodingAgentDriverResult interface built for one-shot coding attempts (working directory, acceptance-criteria file, maxTurns) — not a fit for a conversational streaming tool-calling loop, and building CLI-subprocess streaming parsing is out of scope here. When the resolved provider name is claude-cli or codex-cli (or nothing is configured), the endpoint must emit a single {"type":"error","code":"chat_requires_agent_sdk_provider"|"no_coding_agent_configured", ...} event followed by {"type":"done"}, never a partial/mock/echoed answer.
  • When agent-sdk is configured, chat-grounding.ts drives @anthropic-ai/claude-agent-sdk's query() directly (the same SDK import agent-sdk-driver.ts's defaultQuery already uses), with the session's tool access restricted to connecting against packages/loopover-miner/bin/loopover-miner-mcp.js's MCP server (e.g. as a stdio-connected MCP server in the session's tool config) so the 11 tools' existing implementations are called directly — the 11 tools' logic must not be reimplemented or duplicated inline in this new module.
  • chat-grounding.ts must expose an injectable query-function seam (mirroring agent-sdk-driver.ts's AgentSdkQueryFn injection convention) so every test drives a fake async-iterable instead of making a real model call.
  • The model's system prompt (a constant string in chat-grounding.ts) must explicitly instruct the model to decline questions about wallet, hotkey, coldkey, reward, payout, or trust-score data, stating plainly that none of the available tools expose it — the exact term set from track-record-summary.ts's PUBLIC_FIELD_BLOCKLIST.
  • As a defense-in-depth backstop (the system prompt alone is not enforcement), every outgoing "text" event's text field must be checked against a blocklist of the same terms before being written to the stream; a match must redact/replace the offending chunk rather than forward it verbatim, even though no known code path today produces one.
  • apps/loopover-miner-ui/vite-chat-api.ts must import chat-grounding.ts's exports via packages/loopover-engine's built dist/index.js output (i.e. the same relative-import-into-the-sibling-package style vite-run-state-api.ts already uses for packages/loopover-miner/lib/run-state.js), not by importing TypeScript source under packages/loopover-engine/src/** directly.

Deliverables

  • packages/loopover-engine/src/miner/chat-grounding.ts — provider resolution (via driver-factory.ts), the agent-sdk query() session wired to the 11-tool MCP server, the system-prompt blocklist instruction, and the output-side redaction backstop.
  • Export of chat-grounding.ts's public surface from packages/loopover-engine/src/index.ts.
  • apps/loopover-miner-ui/vite-chat-api.ts — the POST /api/chat Vite middleware plugin, streaming text/event-stream events per the format above.
  • Registration of the new plugin in apps/loopover-miner-ui/vite.config.ts's plugins array, positioned after authPlugin().
  • packages/loopover-engine/test/chat-grounding.test.ts (node:test, mirroring driver-factory.test.ts/agent-sdk-driver.test.ts's injected-fake-driver convention).
  • A root vitest mirror test, test/unit/chat-grounding-engine.test.ts, following the existing test/unit/engine-*.test.ts naming convention (e.g. engine-subprocess-env.test.ts, engine-telemetry-anonymize.test.ts) — required because packages/loopover-engine's own node:test suite is not visible to Codecov; only the vitest-collected paths are.
  • apps/loopover-miner-ui/src/chat-api.test.ts, mirroring the existing apps/loopover-miner-ui/src/run-state-api.test.ts / apps/loopover-miner-ui/src/auth.test.ts pattern.

Test Coverage Requirements

  • packages/loopover-engine/src/miner/chat-grounding.ts and its test/unit/chat-grounding-engine.test.ts vitest mirror are inside Codecov's measured paths (codecov.yml's header comment: coverage is collected over src/**, packages/loopover-engine/src/**, and packages/loopover-miner/lib/**) — target 99%+ patch coverage, branch-counted. Cover both sides of every branch, at minimum: provider-configured (agent-sdk) vs. unconfigured/wrong-provider (claude-cli/codex-cli/none) fail-closed paths; tool-call-requested vs. plain-text-no-tool-call paths; and both the blocklist-triggered-redaction and clean-response paths of the output-side term filter (a SUM()/regex-match-or-not is exactly the kind of both-arms case this repo's coverage bar requires).
  • Add an invariant test asserting the tool-name allowlist passed to the agent-sdk session is exactly the 11 names listed in Context above — no more, no fewer — so a future accidental addition of a 12th tool (or a write-capable one) fails the test, not just code review.
  • apps/loopover-miner-ui/vite-chat-api.ts and its vite.config.ts registration are outside Codecov's coverage.include: codecov.yml explicitly ignores apps/**. This file is not gated by the 99% patch bar, but real vitest tests are still required so the app's own npm run test:ci and its local 90% vitest backstop (per codecov.yml's own comment) pass — cover: unauthenticated request rejected before reaching the handler (mirroring auth.test.ts's existing pattern), malformed/empty messages body rejected, and a fake upstream stream correctly re-emitted as SSE data: lines terminated by "done".
  • Local gate: npm run test:ci plus unsharded npm run test:coverage must be green before this is pushed, per the repo's standing one-shot-PR discipline.

Expected Outcome

A locally-running miner (npm run dev in apps/loopover-miner-ui) exposes an authenticated, streaming POST /api/chat endpoint that answers natural-language questions grounded exclusively in the 11 existing loopover_miner_* read-only tools, using the miner's own already-configured local agent-sdk credentials (no new credential handling), and never emits wallet/hotkey/coldkey/reward/payout/trust-score content even when a user asks for it directly. No write action of any kind is reachable through this endpoint. The chat-rail composer/message-list issues can point at this endpoint with no further backend work.

Links & Resources

  • packages/loopover-miner/bin/loopover-miner-mcp.js — the 11 read-only tools this endpoint may call.
  • packages/loopover-engine/src/miner/driver-factory.ts — provider/credential resolution to reuse.
  • packages/loopover-engine/src/miner/agent-sdk-driver.ts — existing @anthropic-ai/claude-agent-sdk query() precedent and its injected-AgentSdkQueryFn test seam.
  • packages/loopover-engine/src/miner/local-write-tools.ts — the documented no-cloud-write boundary (LOCAL_WRITE_BOUNDARY), context for why this stays read-only.
  • packages/loopover-engine/src/track-record-summary.tsPUBLIC_FIELD_BLOCKLIST, the term set to reuse for the privacy backstop.
  • apps/loopover-miner-ui/vite-auth.ts — the existing auth-cookie gate that already covers this new endpoint automatically.
  • apps/loopover-miner-ui/vite-run-state-api.ts (+ siblings vite-portfolio-queue-api.ts, vite-ledgers-api.ts, vite-governor-api.ts) and apps/loopover-miner-ui/vite.config.ts — the local API middleware convention to follow.
  • apps/loopover-miner-ui/src/run-state-api.test.ts, apps/loopover-miner-ui/src/auth.test.ts — apps-side test convention.
  • packages/loopover-engine/test/driver-factory.test.ts, packages/loopover-engine/test/agent-sdk-driver.test.ts — engine-side node:test convention.
  • test/unit/engine-subprocess-env.test.ts, test/unit/engine-telemetry-anonymize.test.ts — the root vitest-mirror convention Codecov actually measures for packages/loopover-engine.
  • codecov.yml — confirms apps/** is excluded from patch coverage; packages/loopover-engine/src/** and packages/loopover-miner/lib/** are included.
  • Related, not blocking: the chat-rail shell and route-mount issue (mounts the persistent rail in __root.tsx); the composer/message-list/streaming-renderer/typing-indicator UI issues (the consumers of this endpoint); the action-dispatch chat issue (the separately-scoped, later write-capable surface this issue explicitly does not build).

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:featureGittensor-scored feature linked to a feature issue — scores a 0.25x multiplier.help wantedExtra attention is needed

    Projects

    Status
    In Progress

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions