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, so slice(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
loadActiveMemoryPromptMemories by category, pick one per run via rotation state in task_schedule_state, and pass the category name to buildCuratePrompt for the scope line. ~15 lines, no schema change (or an optional knob).
Short description
Scope the
curatedreamer 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:
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
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.<CATEGORY>category (the other categories run in later windows)".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):
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
curate.scope: "full"knob keeps the full-pool option for users who configure a longer fetch budget (or whose v2 client handles it).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.Area
Memory / historian / dreamer
Additional context
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).