Dream Cycle

Your agent forgets everything between sessions. Dream Cycle fixes that. Log what happened during the day (submit_episode_log), trigger a consolidation pass (start_dream), and the next session reads back small, durable lessons (query_dream) instead of re-learning the same mistake. Zero setup — it's on by default on the same https://hypermove.duckdns.org/api/mcp endpoint you already use.

Why this exists

Every MCP session an agent runs starts from zero. If it discovers "the gripper needs a 200ms cooldown before retry" today, that lesson is gone tomorrow — unless you paste the whole transcript back into context, which is slow and expensive at scale.

Dream Cycle is an offline consolidation pass, modeled on how research like "sleep-time compute" and offline memory-consolidation work: cheap, rule-based passes do most of the work; a small LLM call only touches compressed summaries, never raw logs. The output is a handful of short, confidence-scored memories your agent can pull in a single call — cents per day, not dollars.

The 30-second mental model

during the day:     submit_episode_log(agent_id, episodes)   →  cheap, no LLM call
end of day:          start_dream(agent_id, {budget_usd: 0.10}) →  runs the whole pipeline
next session:        query_dream(agent_id, "gripper timeout")  →  "retry after 200ms cooldown"

That's the whole surface. Everything else (resources, prompts, presets, stats) is optional sugar on top of those three calls.

Semantic-preservation canary

Set FEATURE_MCP_DREAM_SEMANTIC_PRESERVATION=true to enable the quality gate. It preserves observations, errors, and tags in clustering/extraction, rejects generic memories with no meaningful source overlap, and adds semantic/truncation counters to get_dream_stats. It is off by default during canary rollout. steps[].result is accepted as a one-release alias for observation_summary and is reported in normalized_fields.

For an affected completed run, call preview_dream_repair first. It proposes generic memory IDs without changing data. apply_dream_repair requires those exact IDs and confirm: true; it quarantines them and replays only the retained source run.

For humans — what you get and what it costs

  • No new infra. Same endpoint, same auth, same bearer token. Dream Cycle just adds 5 tools to what your agent already sees when it calls tools/list: submit_episode_log, start_dream, get_dream_config, query_dream, get_dream_stats. Also 4 resources and 3 prompts — see below.
  • All 5 tools are free. They never hit the paywall or the 5-free-queries/24h limit. Spend is capped by your own budget_usd per cycle (default $0.10), not by HyperMove's tiers.
  • The one metered piece — extraction's LLM call — settles in RLUSD on XRPL, via T54. When the extraction step is priced, the 402 challenge names an amount in RLUSD; you sign a presigned XRPL Payment yourself (no API key, no custodial wallet) and it settles through T54's XRPL x402 Facilitator in 3-5 seconds for sub-cent fees. Full mechanics, real verified testnet transactions, and a runnable demo: see the README's T54 + XRPL section and docs/posts/dream-cycle-rlusd-t54-xrpl-agent-payments.md.
  • Nothing changes if you never call start_dream. Logging with submit_episode_log costs nothing and does nothing until you trigger a cycle — no background jobs, no surprise charges.
  • Your data stays yours. Memories are scoped per agent_id, bound to whichever session first used that ID — a different account can never read or write into it.

For agents — the fastest path to using this

  1. Connect to https://hypermove.duckdns.org/api/mcp the same way you already do (see MCP Gateway if you haven't yet — bearer token from /mcp-connect or the no-wallet flow).
  2. Call tools/list — if Dream Cycle is enabled (default: yes) you'll see the 5 tools below.
  3. Log episodes as you go. Trigger a cycle whenever you want. Query before you plan.

No separate signup, no separate API key, no SDK to install — it's 3 JSON-RPC calls against a tool surface you already discover automatically.

Pick your agent_id once, then it's yours

The first call to submit_episode_log or start_dream for a new agent_id claims it for your session. After that, only your session can write to it — pick a stable, unique string (e.g. your robot's serial number, your bot's slug) and reuse it every day.

1 — Log what happened

curl -s https://hypermove.duckdns.org/api/mcp \
  -H 'authorization: Bearer <token>' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
    "name":"submit_episode_log",
    "arguments":{
      "agent_id":"robot-42",
      "episodes":[{
        "episode_id":"run-2026-07-26-001",
        "agent_id":"robot-42",
        "timestamp":"2026-07-26T09:00:00Z",
        "task_type":"pick_and_place",
        "outcome":"failure",
        "steps":[
          {"action":"grip"},
          {"action":"retry"},
          {"action":"grip_again","error":"gripper timeout"}
        ],
        "tags":["pick_and_place","gripper"]
      }]
    }
  }}'

Returns { "ingested_count": 1, "rejected": [] }. Call this as many times a day as you want — resubmitting the same episode_id is a safe no-op, never a duplicate. No LLM call happens here; this step is free and near-instant.

outcome accepts exactly three values: success, failure, or timeout — there is no "partial" state. For a task that partly succeeded, pick whichever of the three best matches the primary goal (usually success if the main objective was met despite a caveat). This is also declared as an enum on the tool's inputSchema, so MCP clients that render tool schemas surface the valid values without a round-trip.

2 — Trigger a Dream Cycle

curl -s https://hypermove.duckdns.org/api/mcp \
  -H 'authorization: Bearer <token>' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
    "name":"start_dream",
    "arguments":{"agent_id":"robot-42","config":{"budget_usd":0.10,"preset":"balanced"}}
  }}'

Returns { "run_id":"…", "status":"started" } and, once the pipeline finishes inside that same call, the run reaches completed or partial (if the budget ran out — still useful, never wasted). Presets: frugal (cheapest, fewer clusters), balanced (default), thorough (most coverage, higher cost). Call this once a day, or whenever you have a batch of new episodes worth consolidating — there's no scheduler, you decide when.

3 — Query before you plan

curl -s https://hypermove.duckdns.org/api/mcp \
  -H 'authorization: Bearer <token>' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
    "name":"query_dream",
    "arguments":{"agent_id":"robot-42","query":"gripper timeout","top_k":5,"min_confidence":0.5}
  }}'

Returns memories sorted by relevance:

{ "memories": [
  { "memory_id":"…", "type":"error_pattern", "content":"gripper timeout after retry",
    "confidence":0.9, "importance":0.8 }
] }

Read this before your agent starts planning a task — it's the whole point.

Skip the query — read memories as MCP resources instead

If your MCP client supports resources, you can pull the same data with no arguments to fill in — just the right URI for your agent_id:

ResourceContents
hypermove:///agents/{agent_id}/dream/summaryLast run's status + how many memories exist
hypermove:///agents/{agent_id}/dream/rulesHigh-confidence rules
hypermove:///agents/{agent_id}/dream/errorsKnown error patterns
hypermove:///agents/{agent_id}/dream/statsBudget spent, stages completed, memory count

Skip the prompt engineering — use the built-in prompts

PromptWhat it does
dream/summarize_today(agent_id)Plain-language summary of the last cycle
dream/suggest_policy_updates(agent_id, task_type)Concrete policy changes for one task type
dream/compare_before_after(eval_scores_json)Compares your own before/after eval numbers

These are templates, not tools — an MCP client that supports prompts/get renders them pre-filled; there's nothing extra to call.

Check in on a cycle

-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_dream_stats","arguments":{"agent_id":"robot-42"}}}'
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"get_dream_config","arguments":{"agent_id":"robot-42"}}}'

get_dream_statslast_run_at, status, budget_used_usd, stages_completed, memories_count, and (additive) stage_summaries — a coarse, non-content-leaking breakdown of what each stage did:

  • How many episodes preprocessing discarded, and why.
  • How many clusters extraction attempted, skipped, or failed.
  • How many of extraction's candidate insights pruning actually promoted vs. discarded.

If memories_count stays 0 after a run with real episode data, check stage_summaries.pruning_summary first — a nonzero candidates_extracted with candidates_promoted: 0 means extraction genuinely found something that didn't survive pruning (not a silent failure to process your data).

get_dream_config → whatever budget_usd/preset you last set.

Before filing a "this looks broken" report, check two health endpoints — both return a commit field (added 2026-07-27) so you can confirm the fix you're expecting is actually deployed, rather than guessing from a maintainer's say-so:

  • GET /api/mcp/health — the gateway process.
  • GET https://hypermove.duckdns.org/llm/health — the extraction service.

What a fresh agent needs for its first promoted memory

One episode with at least one non-empty steps entry is enough. There is no meaningful confidence/importance threshold standing between a fresh agent and its first promoted memory:

  • Pruning's defaults (min_confidence: 0, min_importance: 0) accept anything.
  • Every new memory is inserted at confidence: 0.5, importance: 0.5 — already well above that floor.
  • A cluster of size 1 (a single unmatched episode) is a valid cluster; there's no minimum cluster size either.

What actually determines whether you get a memory:

  • steps: [] (zero steps) is discarded before extraction ever sees it — this is the one real filter, and it shows up in stage_summaries.preprocessing.discard_reasons as empty_steps. Give every episode at least one steps entry with a real action.
  • Extraction has to find something "rule/preference/error_pattern/fact"-shaped. A single bland episode (e.g. {"action": "did the thing"} with no error, no context) may legitimately produce zero insights for its cluster — that's a correct outcome for low-signal input, not a bug. stage_summaries.extraction.candidates_extracted tells you whether this happened; clusters_failed/clusters_skipped tell you whether it was a transient extraction failure or a budget cutoff instead.
  • stage_summaries is the tool for "why didn't anything promote," not guesswork. Read pruning_summary.candidates_extracted vs. candidates_promoted first, every time, before assuming something is broken.

If a query on a fresh agent still returns nothing after a run with genuinely descriptive episode content (real actions, real errors on failures), check stage_summaries before filing a report — it will show you exactly which stage the signal was lost at.

Confidential extraction on Flare FCC (opt-in, real Coston2 deployment)

By default, Dream Cycle's extraction step calls llm-service directly over plain HTTP. If you want that dispatch attested through HyperMove's own Flare Compute Extension (FCE) — a real, deployed TEE-proxy/extension pair on Coston2, not a mock — opt in per-run:

curl -s https://hypermove.duckdns.org/api/mcp \
  -H 'authorization: Bearer <token>' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
    "name":"start_dream",
    "arguments":{"agent_id":"robot-42","config":{"budget_usd":0.10,"confidential":true}}
  }}'

Gated by FEATURE_MCP_DREAM_CONFIDENTIAL=true (default OFF). Under the hood this routes extraction through the flare.instruct.dispatch tool (GENERIC_AGENT_TASK opType) instead of calling llm-service directly — see flare.instruct.dispatch below for the full mechanics and what's currently real vs. still a hard boundary.

What "confidential" honestly means here — read this before assuming more

This ships "TEE-attested dispatch," not "the LLM call itself runs inside a TEE." The actual token generation still happens in the same non-TEE llm-service process the plain (non-confidential) path calls — the extension's Go handler reaches out to it over the network exactly like any other external HTTP call. If you need the LLM call itself to be confidential, that's explicitly out of scope for what's shipped; nothing here claims otherwise.

Real, independently verifiable, live on Coston2 (as of 2026-08-08):

  • HyperMoveInstructionSender deployed at 0xB4864BB622F3020a5d424ff2CC20738b3327f7E2 — check its bytecode with any RPC client (eth_getCode), it's real.
  • HyperMove's own tee-proxy publicly answers at https://hypermove.duckdns.org/tee-proxy/info — a real, signed TeeInfoResponse from the registered extension, not canned data.
  • flare.instruct.dispatch genuinely reaches the deployed contract and gets real on-chain responses (including real revert reasons while the deployment was mid-setup — nothing was ever silently faked).

The current, honest limit: completing on-chain TEE-machine registration requires a real Google Confidential Space attestation JWT. This deployment runs under SIMULATED_TEE=true (no GCP Confidential VM hardware requested) — its attestation value is the plain string magic_pass, a real, documented "testing outside of the google cloud" sentinel, never a fabricated-but-plausible fake quote. Until real hardware (or a documented simulated-registration path from Flare) exists, start_dream({confidential: true}) will report the honest fcc_dispatch_failed/non-genuine outcome for every cluster — zero cost is ever charged for a non-genuine confidential attempt, matching the same discipline the plain extraction path already uses for a failed/unreachable llm-service call. Check stage_summaries (see "Check in on a cycle" above) to see this distinction directly rather than guessing from silence.

Two different things are both called "confidential" — Coston2 vs. Songbird

Everything above describes HyperMove's own dev-only extension on Coston2 — the code path this deployment actually exercises today. That is deliberately a different system from real Flare Confidential Compute (FCC) on Songbird, and the codebase keeps the two separate on purpose (src/lib/mcp/providers/flare.ts):

  • isHyperMoveTeeExtensionLive(network)true only for network === 'coston2', and only after a real fetch('/health') against HyperMove's own tee-proxy succeeds. This is what everything above (HyperMoveInstructionSender, tee-proxy/info, SIMULATED_TEE=true, magic_pass) is checking. It confirms HyperMove's own extension is configured and reachable — nothing more.
  • isFccLiveOnNetwork(network, client)true only for network === 'songbird', and only after a live on-chain query against Flare's own FlareContractRegistry finds a registered FlareConfidentialCompute entry. As of this writing, this returns false — Flare has not deployed the FCC contract to Songbird yet. This is Flare's own infrastructure timeline, not something this codebase can fix or work around.

Which one a given start_dream({confidential:true}) call actually checks is controlled by DREAM_CONFIDENTIAL_NETWORK (default coston2, in dream/extract.ts). Never conflate a passing Coston2 liveness check with "real Flare FCC is live" — they are different claims about different infrastructure, and the code's own doc comments are emphatic about this distinction for exactly this reason. Either way, confidential: true never silently fails: it always either produces a genuinely attested result, or returns an explicit reason it couldn't (see the fallback-reason table below).

If you request confidential:true and the gate isn't satisfied, you're told — not silently downgraded

FEATURE_MCP_DREAM_CONFIDENTIAL off, or on but with no settled confidential-tier payment, both mean the run happens in plaintext instead of via Flare FCC — this was already true before 2026-08-10. What changed: start_dream's own response now says so, instead of requiring a separate get_dream_config call to discover it after the fact.

These 3 fields appear only when your request actually touched confidential (an explicit true, or an inherited confidential_default of true) — a plain non-confidential call's response is unaffected, byte-identical to before this fix:

{
  "run_id": "...",
  "status": "started",
  "confidential_requested": true,
  "confidential_actual": false,
  "confidential_fallback_reason": "feature_disabled"
}

confidential_fallback_reason is one of:

  • "feature_disabled"FEATURE_MCP_DREAM_CONFIDENTIAL is off.
  • "no_settled_payment" — the flag is on, but no active/consumable confidential-tier paid session exists (this case already returned status: "error" with a payment_challenge before this fix — the 3 fields above are now included on that existing error response too, for a consistent signal either way).

A successful confidential run (flag on + settled payment) reports confidential_actual: true with no confidential_fallback_reason field at all.

flare.instruct.dispatch (standalone tool)

Beyond Dream Cycle, flare.instruct.dispatch is callable on its own — submit either a FINANCIAL_ACTION (SWAP/SETTLE, still an honest not-yet-implemented stub pending Flare's Protocol Managed Wallets interface) or GENERIC_AGENT_TASK (COMPUTE) instruction to HyperMove's Coston2 extension and poll for the result:

curl -s https://hypermove.duckdns.org/api/mcp \
  -H 'authorization: Bearer <token>' \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{
    "name":"flare.instruct.dispatch",
    "arguments":{"opType":"GENERIC_AGENT_TASK","message":{"taskType":"dream.extract","payload":"agent retried gripper pick after timeout"}}
  }}'

Gated by FEATURE_MCP_INSTRUCT_V1 (opt-in). Honest by construction at every stage:

StageHonest outcome if not ready
Feature flag offfeature_disabled
Extension not deployed/configurednot_configured, names the exact env vars to set
On-chain dispatch itself fails (e.g. extension ID not yet set, no TEE machine registered)dispatch_failed, carries the real on-chain revert reason verbatim
Dispatch succeeds, taskType isn't dream.extractThe extension's own honest not-yet-implemented refusal (status 0), never a fabricated success

Full architecture, the exact dependency chain between the indexer/proxy/extension, and every real bug found and fixed while deploying this live: see services/tee-extension/README.md in the repo — it documents the complete Coston2 deployment (contract address, real transaction hashes, the exact hard boundary at TEE-machine registration) in detail.

What's on by default, and how to turn it off

Dream Cycle ships on, same as the rest of the MCP Gateway's v3.0+ tools — no flag to flip to start using it. If you need to disable it (e.g. to roll back a bad deploy):

FEATURE_MCP_DREAM_CYCLE=false

One env var removes all 5 tools, all 4 resources, and all 3 prompts in one flip — nothing else on the gateway changes.

Known limits (Phase 1 — honest, not hidden)

  • start_dream still always runs immediately when called — the manual path is unchanged and always available, on-demand, regardless of the scheduler below.
  • Server-side trigger_criteria enforcement is opt-in, not default (2026-07-27). By default, nothing runs your cycles for you:
    • Set trigger_criteria in config and it's saved either way.
    • It's only enforced automatically if the operator has enabled FEATURE_MCP_DREAM_SCHEDULER=true (off by default — see "Server-side scheduling" below for why).
    • Without it, exactly as before: you decide when to call start_dream.
  • No quality benchmark is published yet. The pipeline is fully functional and budget-safe; the "how much better did my agent get" number isn't measured in this release.
  • Best for motion/episode-style agents. Dream Cycle assumes step-by-step episode logs with an outcome (success/failure/timeout) — it's not a general-purpose memory store for arbitrary unstructured text.

Server-side scheduling (opt-in, added 2026-07-27)

If you set trigger_criteria in start_dream's config (time_window_utc, min_episodes, min_raw_tokens), it's always persisted — but by default nothing reads it back automatically. An operator running this MCP gateway can opt into server-side enforcement:

FEATURE_MCP_DREAM_SCHEDULER=true

Once enabled, an in-process hourly tick checks every agent's trigger_criteria and fires start_dream on your behalf when your conditions are met — no external cron, no script you have to remember to run.

This is deliberately opt-in, not the default (unlike every other Dream Cycle flag) because it's the first feature in this gateway that autonomously spends budget across every registered agent with no per-call human trigger. Two independent ceilings bound worst-case cost per tick regardless of how many agents are configured:

  • DREAM_SCHEDULER_MAX_BUDGET_USD_PER_TICK — default $1.00 total across all agents per tick.
  • DREAM_SCHEDULER_MAX_AGENTS_PER_TICK — default 20.

An agent beyond either ceiling is deferred to the next tick, not silently dropped.

get_dream_stats's triggered_by field ('manual' or 'scheduler') tells you which path produced the last run. If you were already running your own external scheduler (e.g. a cron job calling start_dream directly), nothing about that changes:

  • The two paths are independent and can coexist.
  • Enabling the server-side scheduler on top of an existing external one will just mean both fire (consider disabling one if you do).

Full technical detail, data model, and pipeline internals: docs/prd/dream-cycle-v1.md in the repo.

Was this helpful?