Repository navigation
dreamer curate: scope per memory category (one category per run, rotating) #451
Description
Short description
Scope the curate dreamer task to one memory category per run (rotating across runs) instead of sending the entire pool in a single turn.
What problem does this solve?
The curate turn is scoped to the whole active pool — and that scope is the problem, not the model. A full-pool run puts ~250 memories in the prompt and takes ~150 model calls, so the turn's wall time is 5–19 min depending on server load (admission queuing under concurrent sessions; the decode rate itself is 200+ tok/s). The client fetch layer, however, kills the request at ~300 s (OpenCode client SDK's stock undici headers timeout — tracked at anomalyco/opencode #49044), so every full-pool curate run dies at ~302 s:
| Run | Pool | Result |
|---|---|---|
| run A | 256 | killed at 302 s, committed 7 items, whole pool re-scanned |
| run B | 250 | killed at 302 s, committed 1 item |
| small pool | 39 | completes in 67 s |
Each killed run re-scans the whole pool and commits 1–8 items, so the pool barely drains and the load is self-reinforcing (the scans themselves add to the server pressure that causes the stalls).
The key insight: curate's decisions are all category-local. Merges are category-locked (the system rejects cross-category merges — "Every id in a merge MUST share the same category"), archiving requires a surviving memory "in the same project and category", splits are 1→2 within the same category, rewrites are single-item. No curate decision needs cross-category visibility, so a per-category scope loses zero decision-relevant information.
Proposed solution
- Send one whole category per run (not a fixed N-item slice — see alternatives).
- Rotate the category across runs: a persisted cursor in the task schedule state, or simply
floor(lastRunAt / 3600e3) % categoryCount(no schema change); a retry in the same time window should re-attempt the same category so a killed run's work isn't skipped. - Prompt scope line: "this is the whole of the
<CATEGORY>category (the other categories run in later windows)". - Optional per-task config knob (e.g.
curate.scope: "category" | "full") for users who have configured longer client fetch timeouts.
Measured on a v0.42.3 setup (local 27B model, pool of 246 across 5 categories: 87 / 61 / 56 / 31 / 11):
- 31-item category: 166.6 s, completes clean
- 61-item category: 133.7 s, completes clean
- full pool (249): never completes (302 s kill, ×3)
Per-call latency is load-driven, not model-driven (2.1–7.6 s/call observed on the same full-pool runs at different concurrency levels) — and at any measured pace a single-category turn stays inside the window: the 61-item category is ≈37 calls, 80–280 s.
Alternatives considered
- Rely on a client fetch timeout fix (opencode side) — tracked separately (anomalyco/opencode #49044), but not a dependency of this proposal: opencode is moving rapidly toward v2 (v1 now reads V2 config fields for compatibility), so the current client fetch layer may be superseded before such a fix lands. Per-category scoping stands on its own — it fits the window as it exists today. The optional
curate.scope: "full"knob keeps the full-pool option for users who configure a longer fetch budget (or whose v2 client handles it). - Fixed N-item slice per run (tried first) — the memory loader orders by
category ASC, updated_at DESC, soslice(0, N)starves the larger categories (60% of a 246-item pool never seen) and re-polishes the same N items (their own updates pin them to the top of the ordering). Rejected. - Per-task model/timeout tuning — doesn't change scope; the structural over-window problem remains.
Area
Memory / historian / dreamer
Additional context
- Plugin v0.42.3, OpenCode Desktop 1.18.31, macOS 26.6.2, local 27B model (200+ tok/s).
- The ~302 s kill itself is the OpenCode client SDK's stock 300 s undici headers default on the non-streaming session RPC (headers arrive only at turn completion); no config surface reaches that layer — tracked at anomalyco/opencode #49044 (independent of this proposal; opencode's v2 transition may supersede that client layer).
- Implementation sketch: in the dreamer task runner's curate branch, group
loadActiveMemoryPromptMemoriesbycategory, pick one per run via rotation state intask_schedule_state, and pass the category name tobuildCuratePromptfor the scope line. ~15 lines, no schema change (or an optional knob).
Activity
magic-alfonso commentedon Sep 15, 2026
Adopted as proposed, with two decisions on the open points.
Your category-locality argument holds at source: merge rejects mixed categories structurally, archive requires an active survivor in the same project and category, split is one original update plus one write inside the category, and rewrite is single-item. So a per-category scope loses nothing a full-pool run could decide.
What changed on master:
curateruns one whole category per scheduled window, rotating through the five taxonomy categories in declared order. The prompt's scope line names the category and says the others run in later windows./ctx-dream, the dashboard) report the category in scope and its live count rather than the whole pool.curate.scopeknob: a full-pool run has no decision the category run lacks, so there is nothing for the knob to select.On your reporter distribution (87 / 61 / 56 / 31 / 11) the prompt goes from 246 memories per run to at most 87, which is inside the window at every per-call latency you measured. OpenCode and Pi share the executor, so both rotate identically.
Ships in the next release. Thanks for the measured runs and the category-local analysis; it made this a short fix.
magic-alfonso commentedon Sep 16, 2026
Shipped in v0.42.5: curate now runs one memory category per scheduled window, rotating through the taxonomy, so the pool actually drains on local models instead of timing out on the full set. All three packages are on npm. Thanks for the measurements — they set the design.