Summary
A "dream" is a periodic background run that distills recent transactional memory into long-term artifacts under an explicit token budget, then updates a retrieval index used at the next session's startup. openwop already has the halves — RFC 0012 defines host-managed _compaction_ (lossy distillation + the memory.compacted event), and RFC 0052 defines _scheduled_ run initiation — but nothing binds them, pins a _token budget_, or defines the _retrieval index_ that closes the loop back to startup. This RFC adds an additive memory.distillation capability that composes them: a scheduled compaction run reads memory (RFC 0004), applies a mandatory token budget, runs RFC 0012 compaction, writes a stable archive plus a memory-index manifest (a workspace file per RFC 0059), and emits the existing memory.compacted event with an additive optional distillation sub-object carrying budget metadata — not a parallel memory.distilled event.
Motivation
The feature set's product story: "an agent runs nightly memory distillation so it can synthesize learnings without manual prompting… cron @ 23:00 triggers a DREAM-ROUTINE with token budgets; output appended to archives and indices updated for next-day retrieval." The constraints section is explicit that token budgets are mandatory — "token budgets / context windows can cause memory retrieval failures if misconfigured; dreams must have explicit budgets."
Today RFC 0012 compaction exists and the examples/hosts/postgres host even implements runCompaction(), but: (a) nothing schedules it — it is trigger-on-demand only; (b) there is no token-budget contract, so distillation can blow a context window; (c) there is no _index_ the next run reads, so distilled output is written but not made discoverable at startup. The apps/workflow-engine demo neither advertises nor wires any of it.
The spec is the right place because "distillation ran on schedule, stayed within its token budget, produced a stable archive, and updated the index the next run reads" are interop + correctness guarantees an operator depends on for unattended overnight runs.
Proposal
§A — capabilities.schema.json: memory.distillation sub-block (additive, nested under memory)
"memory": {
"properties": {
"compaction": { "...": "RFC 0012 (unchanged)" },
+ "distillation": {
+ "type": "object",
+ "description": "RFC 0062. Scheduled, token-budgeted distillation built on compaction (RFC 0012) + scheduling (RFC 0052). A distillation run is a compaction run with a budget + schedule + index.",
+ "required": ["supported"],
+ "additionalProperties": false,
+ "properties": {
+ "supported": { "type": "boolean" },
+ "maxTokenBudget": { "type": "integer", "minimum": 1, "description": "Largest per-run distillation token budget the host honors." },
+ "scheduled": { "type": "boolean", "description": "Host can initiate distillation on a schedule (requires host.scheduling, RFC 0052). Distillation MAY also run on-demand without scheduling." },
+ "indexEmitted": { "type": "boolean", "description": "Host writes a retrievable memory-index manifest after distillation." },
+ "tokenizerName": { "type": "string", "description": "Identifier of the tokenizer the budget is counted against (e.g. `claude`, `gpt-4`). The budget is best-effort-honest per this tokenizer, not byte-exact." }
+ }
+ }
}
}
§B — distillation run contract (normative, when memory.distillation.supported: true)
A distillation run — whether scheduled (RFC 0052 schedule trigger targeting the distillation handler) or on demand — MUST:
- Read the source
memoryRef's entries via the RFC 0004 read snapshot (deterministic input). - Apply a token budget — a
tokenBudget(≤ advertisedmaxTokenBudget), supplied via therun-options.mdreserved keydistillation.tokenBudget(clamped tomaxTokenBudget); absent ⇒ the host MUST default tomaxTokenBudget. The budget caps _input + output_ token accounting, counted against the advertisedtokenizerName(best-effort-honest). If the source cannot be meaningfully distilled within the budget the run MUST fail withtoken_budget_exceededand write no partial archive (atomic). (token_budget_exceededis registered inrest-endpoints.mdby this RFC — it is present in the SDK error vocabulary but not yet in the spec's error-code list.) - Distill via the RFC 0012 compaction mechanism (carrying forward the SR-1 redaction invariant — a distilled archive MUST NOT re-expose a secret the sources had redacted).
- Write a stable archive — the distilled output is an immutable, addressable artifact (byte-stable for a given source set + budget, so it is reproducible + auditable).
- Update the memory-index manifest when
indexEmitted: true— a retrievable summary the next session loads at startup. Per RFC 0059 §"Unresolved", the index is stored as a workspace file (MEMORY-INDEX.json); updating it emitsworkspace.updated(RFC 0059), not a bespoke index event. - Emit the existing
memory.compactedevent (RFC 0012) — extended with an additive optionaldistillationsub-object. Thetriggerfield stays within RFC 0012's closed enum (host-managedfor a scheduled distillation — the host owns the schedule; clients don't request it on-demand):
"memoryCompacted": {
"properties": {
"memoryRef": { "...": "unchanged" },
"trigger": { "enum": ["host-managed", "client-requested", "both"], "...": "unchanged" },
"sourceCount": { "...": "unchanged" }, "byteSize": { "...": "unchanged" },
+ "distillation": {
+ "type": "object",
+ "description": "RFC 0062. Present when this compaction is part of a budgeted distillation run.",
+ "properties": {
+ "tokenBudget": { "type": "integer", "minimum": 1 },
+ "tokensUsed": { "type": "integer", "minimum": 0 },
+ "indexUpdated": { "type": "boolean" }
+ }
+ }
}
}
Positive example. Nightly schedule (RFC 0052, 0 23 *) fires a distillation run with tokenBudget: 8000; it distills 412 entries into one archive using 7,611 tokens, updates the index (workspace.updated), emits memory.compacted { trigger: 'host-managed', distillation: { tokenBudget: 8000, tokensUsed: 7611, indexUpdated: true } }. Next morning's run loads the index at startup. Negative example. tokenBudget: 100 against a 400-entry corpus that cannot be distilled under 100 tokens → token_budget_exceeded { details: { budget: 100, minimumRequired: ~900 } }; no partial archive (atomic).
§C — relationship to the cohort
- RFC 0012 — distillation _is_ compaction with a budget + schedule + index wrapped around it; it reuses
memory.compacted(+ the optionaldistillationsub-object), not a new event. RFC 0012's block + the existing payload are unchanged for consumers that ignore the sub-object. - RFC 0052 — supplies the schedule;
scheduled: truerequireshost.scheduling. - RFC 0059 — the index manifest is a workspace file; its update rides
workspace.updated. - RFC 0004 — source reads use the memory snapshot; CTI-1 tenant isolation holds for the archive.
Compatibility
Additive. New optional memory.distillation sub-block; new optional distillation.tokenBudget reserved key; an additive optional distillation sub-object on the existing memory.compacted payload (its required array unchanged; consumers that only know RFC 0012 ignore the sub-object). The trigger enum is unchanged (distillation emits host-managed). token_budget_exceeded is a new error code (no existing code changes meaning). Hosts without the block keep on-demand compaction (RFC 0012) or no memory. No existing surface changes. No conformance pass invalidated.
Conformance
distillation-shape.test.ts—memory.distillationblock validates;maxTokenBudgetpositive; thedistillationsub-object onmemory.compactedvalidates. (Always-on.)distillation-token-budget.test.ts— within budget:memory.compactedcarriesdistillation.tokensUsed ≤ tokenBudget; un-meetable budget →token_budget_exceeded, no partial archive. (Gated onmemory.distillation.supported.)distillation-stable-archive.test.ts— same source set + budget ⇒ byte-stable archive. (Gated.)distillation-index-roundtrip.test.ts— after distillation the memory-index workspace file is retrievable + aworkspace.updatedevent fired. (Gated onindexEmitted+host.workspace.supported.)distillation-secret-carryforward.test.ts— a redacted secret in source memory stays redacted in the archive (SR-1). (Gated; composes with the redaction suite.)
The four gated scenarios drive the memory-distillation seam POST /v1/host/sample/memory/distill (request { memoryRef, tokenBudget?, sources?, indexEmitted?, includeSecretCanary? } → { event, archiveChecksum, indexUpdated, indexFile? }), specified in host-sample-test-seams.md §"Open seams". A host advertising capabilities.memory.distillation.supported: true wires it to light them up; they soft-skip on 404 until then.
Alternatives considered
- A new
memory.distilledevent (the author's first draft). Rejected — it duplicatesmemory.compacted(RFC 0012), splitting the compaction observability stream so an existingmemory.compactedconsumer misses scheduled distillations. Reusingmemory.compacted+ an additive optionaldistillationsub-object is the lower-surface-area path (the RFC 0058cap.breachedprecedent). - Add
"scheduled"to thetriggerenum. Rejected — RFC 0012'striggerenum is closed and discriminates _who controls the trigger_ (host vs client), not _how the host schedules it_. A scheduled distillation ishost-managed; its scheduled nature is evident from thedistillationsub-object + the initiatingscheduletrigger. - A distillation pipeline independent of RFC 0012. Rejected — duplicates the compaction mechanism + its SR-1 carry-forward invariant.
Unresolved questions
- Token accounting authority. The budget is counted against the advertised
tokenizerName, best-effort-honest (±margin), not byte-exact. Conformance uses a reference tokenizer with a tolerance. Confirm the tolerance before Active. - Archive retention / GC. How long must archives persist; can distillation distill prior archives (recursive)? Proposed: advertise
archiveRetention; recursive allowed, each level re-checks SR-1. Decide before Active. - Index format.
MEMORY-INDEX.json(machine-loaded, normative) vs. a human-editable.mdsibling. Proposed JSON normative. Confirm with RFC 0059.
Phase-0 resolution (architect ruling, 2026-05-25)
Per docs/autonomous-agent-runtime-plan.md §8 — Unresolved questions resolved (wire shape pinned; Active-ready pending schema + prose):
- Token accounting authority → counted against an advertised
tokenizerName, best-effort-honest; conformance tolerance ±10%. - Archive retention / GC → advertise
archiveRetention(ISO-8601 duration); recursive distillation allowed, each level re-checks SR-1. - Index format →
MEMORY-INDEX.jsonnormative (machine-loaded); optional human-editable.mdsibling. The additivedistillationsub-object onmemory.compactedis confirmed non-breaking (ruling A;additionalProperties:true);token_budget_exceededis a new error code registered inrest-endpoints.md(ruling C).
Implementation notes (non-normative)
examples/hosts/postgresalready hasrunCompaction(); the reference wiring adds a budget guard, the archive write, the index workspace-file write, and the scheduled-trigger binding — emitting the extendedmemory.compacted.apps/workflow-engineadvertisesmemory.distillation+ wires the same. Depends on RFC 0052 (schedule) + RFC 0059 (index file). Gate RFC 0062Acceptedon RFC 0059 reaching at least a pinned workspace-file schema.- SR-1 carry-forward lands at the RFC 0012 layer; no _new_ invariant, but the distillation conformance scenario re-asserts it.
Acceptance criteria
- [x]
agent-memory.mddistillation section (budget + stable archive + index + scheduled-trigger binding) +distillation.tokenBudgetreserved key inrun-options.md+token_budget_exceededregistered inrest-endpoints.md. - [x]
memory.distillationblock (schema) + additive optionaldistillationsub-object onmemoryCompacted(run-event-payloads schema). No new event type. - [x] Conformance: shape always-on; budget/archive/index/secret-carryforward capability-gated — all five
distillation-*.test.tsscenarios live + green against the in-memory host. - [x] CHANGELOG entry under
[1.1.4 — unreleased]. - [x] Milestone 2 — reference-host enforcement (
Active → Accepted). The in-memory reference host (examples/hosts/in-memory) advertisescapabilities.memory.distillation { supported, maxTokenBudget, scheduled: false, indexEmitted: true, tokenizerName, archiveRetention }and implements thePOST /v1/host/sample/memory/distillseam: §B runs a budgeted distillation (tokensUsed ≤ tokenBudget; a budget below the corpus minimum fails atomically withtoken_budget_exceededand writes no partial archive), produces a byte-stable JCS+SHA-256archiveChecksum(same sources ⇒ same digest), writes a content-freeMEMORY-INDEX.jsonworkspace file via the RFC 0059 store onindexEmitted, and SR-1-scrubs the corpus before archiving so a redacted secret never re-appears. Thememory.compactedevent carries the additivedistillationsub-object — no new event type, no new SECURITY invariant (SR-1 holds at the RFC 0012 layer). On-demand only:scheduled: false(nocapabilities.schedulingon this host — a scheduled trigger composes RFC 0052 on a scheduling host); the host has no productionMemoryAdapter, so distillation is seam-demonstrated.
References
RFCS/0012-memory-compaction-profile.md— the compaction mechanism +memory.compactedevent this reuses + extends.RFCS/0052-scheduling-and-time-based-triggers.md— the schedule that initiates a dream.RFCS/0059-agent-workspace.md— the durable layer the index manifest rides (workspace.updated).spec/v1/agent-memory.md— read snapshot + SR-1 / CTI-1 invariants.