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 samehttps://hypermove.duckdns.org/api/mcpendpoint 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_usdper 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
Paymentyourself (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 anddocs/posts/dream-cycle-rlusd-t54-xrpl-agent-payments.md. - Nothing changes if you never call
start_dream. Logging withsubmit_episode_logcosts 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
- Connect to
https://hypermove.duckdns.org/api/mcpthe same way you already do (see MCP Gateway if you haven't yet — bearer token from /mcp-connect or the no-wallet flow). - Call
tools/list— if Dream Cycle is enabled (default: yes) you'll see the 5 tools below. - 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:
| Resource | Contents |
|---|---|
hypermove:///agents/{agent_id}/dream/summary | Last run's status + how many memories exist |
hypermove:///agents/{agent_id}/dream/rules | High-confidence rules |
hypermove:///agents/{agent_id}/dream/errors | Known error patterns |
hypermove:///agents/{agent_id}/dream/stats | Budget spent, stages completed, memory count |
Skip the prompt engineering — use the built-in prompts
| Prompt | What 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_stats → last_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 instage_summaries.preprocessing.discard_reasonsasempty_steps. Give every episode at least onestepsentry with a realaction.- 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_extractedtells you whether this happened;clusters_failed/clusters_skippedtell you whether it was a transient extraction failure or a budget cutoff instead. stage_summariesis the tool for "why didn't anything promote," not guesswork. Readpruning_summary.candidates_extractedvs.candidates_promotedfirst, 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):
HyperMoveInstructionSenderdeployed at0xB4864BB622F3020a5d424ff2CC20738b3327f7E2— check its bytecode with any RPC client (eth_getCode), it's real.- HyperMove's own
tee-proxypublicly answers athttps://hypermove.duckdns.org/tee-proxy/info— a real, signedTeeInfoResponsefrom the registered extension, not canned data. flare.instruct.dispatchgenuinely 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)—trueonly fornetwork === 'coston2', and only after a realfetch('/health')against HyperMove's owntee-proxysucceeds. 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)—trueonly fornetwork === 'songbird', and only after a live on-chain query against Flare's ownFlareContractRegistryfinds a registeredFlareConfidentialComputeentry. As of this writing, this returnsfalse— 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_CONFIDENTIALis off."no_settled_payment"— the flag is on, but no active/consumableconfidential-tier paid session exists (this case already returnedstatus: "error"with apayment_challengebefore 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:
| Stage | Honest outcome if not ready |
|---|---|
| Feature flag off | feature_disabled |
| Extension not deployed/configured | not_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.extract | The 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=falseOne 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_dreamstill always runs immediately when called — the manual path is unchanged and always available, on-demand, regardless of the scheduler below.- Server-side
trigger_criteriaenforcement is opt-in, not default (2026-07-27). By default, nothing runs your cycles for you:- Set
trigger_criteriainconfigand 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.
- Set
- 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=trueOnce 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?