Repository navigation
map-memories: even time-slice allocation starves batches when backlog is large (zero progress loop) #356
Description
Short description
The map-memories dreamer task divides its whole task deadline evenly across the remaining batches. With MAP_BATCH_SIZE = 80 (hardcoded) and the schema-default timeout_minutes: 20, per-batch prompt budget = 1200s ÷ ceil(backlog/80). Any backlog above ~880 memories (≥12 batches) drops the budget to ≤100s. An agentic model executing map-memories through tool calls needs minutes per batch. Result: every batch times out, nothing is ever committed, and the loop repeats until the deadline decays — a zero-progress feedback loop that worsens as the backlog grows.
What happened?
Summary
The map-memories dreamer task divides its whole task deadline evenly across the remaining batches:
// dist/index.js (@cortexkit/opencode-magic-context@0.38.0), mapMemories():
const deadline = startedAt + config.timeoutMinutes * 60 * 1000;
...
const remainingMs = Math.max(0, args.deadline - Date.now());
const batchesRemaining = batches.length - i;
const sliceMs = Math.max(1, Math.floor(remainingMs / batchesRemaining));
// → promptSyncWithValidatedOutputRetry(..., { timeoutMs: sliceMs })With MAP_BATCH_SIZE = 80 (hardcoded) and the schema-default timeout_minutes: 20, per-batch prompt budget = 1200s ÷ ceil(backlog/80). Any backlog above ~880 memories (≥12 batches) drops the budget to ≤100s. An agentic model executing map-memories through tool calls needs minutes per batch. Result: every batch times out, nothing is ever committed, and the loop repeats until the deadline decays — a zero-progress feedback loop that worsens as the backlog grows.
This is the out-of-the-box configuration. At reproduction time our config contained no timeout_minutes key at all (config-at-starvation-repro.jsonc); we verified against upstream source that,
(a) the official setup flow never asks about or writes this key — dreamer-setup.ts persists only model and per-task schedule — and
(b) the effective default is plugin-internal (installed v0.38.0: schema .default(20); upstream HEAD: runtime ?? 20 in task-config.ts).
The only code path that writes it into a user config is a legacy v1→v2 migration gated on an old key our config never had. No user action produces this state; every fresh install lands in it.
Environment
| Component | Value |
|---|---|
| Harness | OpenCode TUI, local server (opencode --port 5012) |
| OpenCode version | 1.18.21 |
| Plugin | @cortexkit/opencode-magic-context@0.38.0 (pinned in opencode.jsonc "plugin" array) |
| Dreamer/sidekick/historian model | nvidia/nvidia/nemotron-3-ultra-550b-a55b (NVIDIA NIM, free tier) |
| OS | Linux (x86_64) |
| Backlog at observation | remaining=916→921 unmapped inputs ⇒ batches=12 (ceil(921/80)=12, matches logs) |
Steps to reproduce
- Fresh install / default config: do not set
timeout_minutes(the official setup flow never writes it — verified indreamer-setup.ts; the starving default is what every new user gets). - Accumulate a memory backlog ≥ ~880 items (⇒ ≥12 batches at MAP_BATCH_SIZE=80 ⇒ ≤100s slices at the default budget; observed: 916–921 items → 12 batches).
- Run the dreamer on a model whose per-batch tool-loop cost exceeds
deadline/batches(~100s here). - Observe
map-memories batch failed: "prompt timed out after <sliceMs>"repeating every tick with no committed mappings.
Actual behavior (primary evidence)
Plugin log (magic-context.log), all lines from one day, verbatim timestamps and slice values:
[2026-08-22T02:01:40Z] batch failed: "prompt timed out after 99984ms"
[2026-08-22T02:03:21Z] batch failed: "prompt timed out after 99915ms"
[2026-08-22T02:05:02Z] batch failed: "prompt timed out after 99804ms"
[2026-08-22T02:06:43Z] batch failed: "prompt timed out after 99712ms"
[2026-08-22T02:08:23Z] batch failed: "prompt timed out after 99621ms"
[2026-08-22T02:10:03Z] batch failed: "prompt timed out after 99523ms"
[2026-08-22T02:11:44Z] batch failed: "prompt timed out after 99237ms"
[2026-08-22T02:13:24Z] batch failed: "prompt timed out after 99059ms"
[2026-08-22T02:15:04Z] batch failed: "prompt timed out after 98658ms"
[2026-08-22T02:16:44Z] batch failed: "prompt timed out after 98336ms"
[2026-08-22T02:18:23Z] batch failed: "prompt timed out after 97914ms"
[2026-08-22T02:20:00Z] batch failed: "prompt timed out after 96230ms"
Twelve consecutive attempts, slices monotonically decaying 100s→96s exactly as the formula predicts (the shared 1200s deadline depletes while batchesRemaining stays constant because no batch ever succeeds). Task summaries throughout: map-memories: mapped=0 ... complete=false.
Backlog-growth proof of the feedback loop: remaining=916 (01:54Z) → remaining=921 (02:20Z) — the backlog grew during hours of retries because new memories were indexed while none were ever mapped.
Knob verification (proves the mechanism, not coincidence): after setting timeout_minutes: 60 and restarting, the very next timeout was 299979ms — i.e., 3600000ms ÷ 12 batches = 300000ms minus 21ms of pre-slice elapsed time. Byte-for-byte match with the traced formula.
Natural experiment corroborating the batch-count dependence: two days earlier, the same task with a small backlog ran as a single batch and completed cleanly:
[2026-08-20T20:40:54Z] map-memories: mapped=21 independent=9 batches=1 remaining=0 complete=true
With batchesRemaining = 1, the slice equals the entire remaining task window — ample time. Same model class of workload, only the batch count differs: 1 batch → success; 12 batches → 12 consecutive ~100s timeouts.
Expected behavior
Per-batch budgets should be derived from expected batch cost (or adapted), and/or the system should detect sliceMs < plausible batch cost early and fail loudly / reduce scope instead of silently burning the entire task window on doomed prompts. At minimum, progress made inside a timed-out batch should not be wholly discarded.
Suggested fixes (any one suffices to close the loop)
- Cost-aware slicing: estimate per-batch cost (rolling average of completed batch durations) and set
timeoutMinutesguidance or split batches socost_estimate ≤ slice. - Adaptive batching: if measured pace exceeds slice, halve the batch (re-chunk the 80 inputs) instead of failing.
- Early-exit with warning: when
remainingMs / batchesRemaining < MIN_PLAUSIBLE_BATCH_MS, abort the run up-front with an actionable message ("backlog too large for task budget; raisetimeout_minutes"). - Docs: surface
MAP_BATCH_SIZE/ per-task budget interplay in the schema description fortimeout_minutes.
Workaround used (works, but is manual)
~/.config/cortexkit/magic-context.jsonc:
"dreamer": {
"tasks": {
"map-memories": {
"schedule": "0 2 * * *",
"timeout_minutes": 120 // 7200s ÷ 12 batches = 600s/batch
}
}
}This eliminated timeouts entirely (verified live: previous 300s slices produced 299979ms timeouts; after the change the first batch ran ~8 min and reached validation). But it only shifts the cliff — the starvation returns whenever backlog × per-batch cost outgrows the window again. Note that the existence of this workaround does not downgrade the issue: the defect is that the default configuration (key absent, which is what setup produces and what most users will run) has no viable path once the backlog outgrows the silent 20-minute budget.
Upstream tracker check (no duplicate)
no prior issue covers even-slice allocation, timeout_minutes adequacy, or batch-count starvation. The formula is still present at master HEAD (map-memories.ts:181-182) and in the latest release v0.38.1 (2026-08-21, changelog does not touch it). Tangentially related, not duplicates: #272 (dreamer progress visibility), #270 (sub-agent empty result).
Notes for maintainers
- Two larger slices observed under default config (
353823ms,390045ms) are also formula-consistent: those runs evidently had fewer eligible inputs (≤3 batches ⇒ ~240 inputs), implyingloadUnmappedInputseligibility varies between runs. Worth documenting alongside the fix.
Attachment manifest (this package)
| File | Contents |
|---|---|
| batch-failures-default-20min-budget-full-lines.log | 14 full failure lines under default budget — incl. the monotonic 99984→96230ms decay series and two large-slice outliers (353823/390045ms, formula-consistent with reduced eligible-batch counts) |
| batch-failures-60min-budget-full-lines.log | 3 lines at 60-min budget: 299979ms == floor(3600s/12)−21ms (exact prediction match), then 297582/297439ms |
| map-memories-progress-summaries-all.log | All task summaries: mapped=0 throughout; remaining 916→921 (backlog growth during zero-progress retries) |
| dist-slice-allocation-code.txt | Verbatim excerpts from running @0.38.0 dist/index.js: mapMemories() loop, deadline line, MAP_BATCH_SIZE=80 |
| config-current-after-workaround.jsonc | Full config as of filing (timeout_minutes: 120 workaround in place) |
| config-at-starvation-repro.jsonc | Exact on-disk config during the starvation reproduction (model refs fixed; no timeout_minutes key → default 20-min budget) |
| config-original-before-any-fix.jsonc | Provenance snapshot: state before any same-day fix (broken model refs, no timeout key) |
| CONFIG-STATE-TIMELINE.txt | Provenance chain linking each config snapshot to the failure regime it produced |
| environment.txt | Environment matrix + reproduction timeline (UTC) |
| magic-context-issue-20260822-150700.md | Official doctor --issue output (auto-collected diagnostics: config paths, plugin cache v0.37.0-cached/v0.38.1-latest, storage, sanitized log tail) |
Diagnostics
Plugin version
0.36.0
OpenCode version
1.18.20
Platform
linux x64
Client
OpenCode TUI (CLI)
Activity
octal-illumination commentedon Aug 22, 2026
I was not able to attach the magic context output, it kept showing 'Error unable to create Issue with copy pasting
npx @cortexkit/magic-context@latest doctor --issueoutput. So I am adding that info in this comment.Title
map-memories: even time-slice allocation starves batches when backlog is large (zero progress loop)
Description
Summary
Environment
Configuration
Config from
~/.config/cortexkit/magic-context.jsonc:{ "$schema": "https://raw.github<USER>content.com/cortexkit/magic-context/master/assets/magic-context.schema.json", "historian": { "model": "nvidia/nvidia/nemotron-3-ultra-550b-a55b" }, "dreamer": { "model": "nvidia/nvidia/nemotron-3-ultra-550b-a55b", "tasks": { "map-memories": { "schedule": "0 2 * * *", "timeout_minutes": 120 }, "verify": { "schedule": "0 3 * * *" }, "verify-broad": { "schedule": "0 4 * * 0" }, "curate": { "schedule": "0 4 * * 0" }, "classify-memories": { "schedule": "0 6 * * *" }, "retrospective": { "schedule": "0 5 * * *" }, "maintain-docs": { "schedule": "" }, "evaluate-smart-notes": { "schedule": "0 3 * * *" }, "review-user-memories": { "schedule": "0 3 * * *" }, "promote-primers": { "schedule": "0 3 * * *" }, "refresh-primers": { "schedule": "0 3 * * *" } } }, "sidekick": { "model": "nvidia/nvidia/nemotron-3-ultra-550b-a55b", "timeout_ms": 30000 }, "memory": { "git_commit_indexing": { "enabled": false } } }Diagnostics
OpenCode installations
~/.opencode/bin/opencode~/.config/ai.opencode.desktop/opencode.settingsConfig paths
{ "configDir": "~/.config/opencode", "opencodeConfig": "~/.config/opencode/opencode.jsonc", "opencodeConfigFormat": "jsonc", "magicContextConfig": "~/.config/cortexkit/magic-context.jsonc", "tuiConfig": "~/.config/opencode/tui.jsonc", "tuiConfigFormat": "jsonc", "omoConfig": null }magic-context.jsonc flags
{ "$schema": "https://raw.github<USER>content.com/cortexkit/magic-context/master/assets/magic-context.schema.json", "historian": { "model": "nvidia/nvidia/nemotron-3-ultra-550b-a55b" }, "dreamer": { "model": "nvidia/nvidia/nemotron-3-ultra-550b-a55b", "tasks": { "map-memories": { "schedule": "0 2 * * *", "timeout_minutes": 120 }, "verify": { "schedule": "0 3 * * *" }, "verify-broad": { "schedule": "0 4 * * 0" }, "curate": { "schedule": "0 4 * * 0" }, "classify-memories": { "schedule": "0 6 * * *" }, "retrospective": { "schedule": "0 5 * * *" }, "maintain-docs": { "schedule": "" }, "evaluate-smart-notes": { "schedule": "0 3 * * *" }, "review-user-memories": { "schedule": "0 3 * * *" }, "promote-primers": { "schedule": "0 3 * * *" }, "refresh-primers": { "schedule": "0 3 * * *" } } }, "sidekick": { "model": "nvidia/nvidia/nemotron-3-ultra-550b-a55b", "timeout_ms": 30000 }, "memory": { "git_commit_indexing": { "enabled": false } } }Plugin cache
{ "path": "~/.cache/opencode/packages/@cortexkit/opencode-magic-context@latest", "cached": "0.37.0", "latest": "0.38.1" }Storage
{ "path": "~/.local/share/cortexkit/magic-context", "exists": true, "context_db_size": "282.9 MB" }Recent sessions
No recent OpenCode sessions found (or OpenCode DB unavailable on this runtime).
Historian dumps
(Metadata only — XML content is not included in this report.)
Dumps are stored per-project under
<project>/.cortexkit/magic-context/historian/.{ "byProject": [], "legacyDumps": { "dir": "/tmp/opencode/magic-context/historian", "count": 0, "recent": [] } }Historian failures (session_meta)
Note: this counter RESETS to 0 on every successful run — see 'Historian runs' below for the durable history.
No sessions with historian failures.
Historian runs (durable telemetry)
Per-session success/failure/no-op counts from
historian_runs(never reset).No historian runs recorded (or schema predates v24).
Log file
Historian failure signals (log, sanitized)
Recent errors (last 20, sanitized)
Log (last 400 lines, sanitized)
magic-alfonso commentedon Aug 22, 2026
Confirmed — the mechanism is exactly as you traced it.
sliceMs = remaining / batchesRemainingmakes the per-batch budget a function of backlog size, so past roughly 12 batches every slice drops below what an agentic batch can finish, every batch times out, and the run banks nothing — a zero-progress loop that a growing backlog only deepens. Thank you for reading the dist down to the line; the diagnosis needed no correction.The fix will follow the shape we already use for the compress-cues starvation class: a per-batch floor budget (each batch gets a minimum viable slice, and the run processes however many floored batches fit in the deadline rather than shrinking slices to fit them all), banked partial progress (map-memories is already resumable, so committed batches persist across runs), and a consecutive-timeout circuit breaker so a wedged model ends the run loudly instead of burning the whole deadline. With that, a large backlog costs more runs instead of zero progress.
Also noted from your report:
doctor --issuefailing to create the issue when you pasted its output is itself a defect — tracked separately, and the report you filed despite it is appreciated. The fix for this issue will be linked here; it targets the next release.octal-illumination commentedon Aug 22, 2026
Ah, so this is a known issue, also
dotor --issueusually showsopencodenot installed, if that's of any use. Thank you for the prompt response.fix(#356): map-memories per-batch floor (240s) + fit-stop + banked re…
magic-alfonso commentedon Aug 22, 2026
Fix is on master (21d36dc): each batch now gets a 240s floor instead of an even share of the remaining deadline, the run processes as many floored batches as fit and stops cleanly (banking committed batches for the next run to resume), and two consecutive batch timeouts abort the run loudly instead of burning the deadline. The zero-progress loop you traced is the primary regression test, including a proof that reverting to pure division reds it. Ships in the next release.
magic-alfonso commentedon Aug 22, 2026
Shipped in v0.38.1.
magic-alfonso commentedon Aug 22, 2026
Correction to the close note above: this fix merged after v0.38.1 was cut yesterday, so it is NOT in v0.38.1 — it ships in v0.39.0, which is being prepared now. Reopening until it is actually released.
magic-alfonso commentedon Aug 23, 2026
Shipped in v0.39.0 (npm + GitHub release are live). Thanks again for the precise reports — all three of your findings from that morning landed in this release.