Skip to content

v0.44.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 11:46
· 2886 commits to master since this release

[!IMPORTANT] > On OpenCode 1, 0.43.x could let OpenCode's own automatic compaction run alongside Magic Context (#538). If no OpenCode config file you use has a compaction key, 0.43.0 to 0.43.2 kept Magic Context enabled while OpenCode 1's automatic compaction, which is on by default, also ran, so both managed the context window. Before 0.43.0, Magic Context switched itself off in that case. From this release, whenever Magic Context manages compaction, it turns OpenCode 1's automatic compaction off for its own OpenCode instance. It no longer depends on reading your config files to do this. Only compaction.auto is changed: manual /compact still works and your prune setting is kept. If you set compaction.enabled: false in Magic Context's config, native compaction is left alone. If you installed with npx @cortexkit/magic-context setup, your config already has compaction.auto: false and you were not affected. If you stay on 0.43.x, add this to your opencode.json(c) to get the same result: > > jsonc > "compaction": { "auto": false, "prune": false } >

This release fixes the rest of what #538 found on long sessions: the context limit changing between models, an extra cache rebuild after a native /compact, and a late usage reading overwriting a newer one. SQLite writes now wait for a lock held by another process instead of failing (#533). The first OpenCode 2 request on a large converted session no longer blocks for minutes (#536). doctor stops offering an OpenCode 2 reconversion that deleted messages (#493). Dropped tool calls no longer show models a placeholder they copy (#513). Queued drops now apply on the first turn after a long idle (#545). Images read by tools on OpenCode 2 reach the model as images, not as base64 text (#539). History search no longer depends on memory.enabled (#543). Transform mode rust now runs on OpenCode 2, and on OpenCode 1 and 2 it runs the historian and dreamer in the host by default. On OpenCode 2, a turn refused because the database can't be opened now says why and goes through again once the cause is gone, without a restart (#546). On OpenCode 1 builds that give plugins a client with a request timer, dreamer tasks no longer fail after five to six minutes. ck-mc refuses a store.db newer than itself instead of half-working on it (#540). Pi tools now report their errors as errors (#547).

Upgrading

  • The schema fence moves from 90 to 91. One migration runs on first open:
    • v91 adds a memory_embedding_watermarks table. It records, per project, the newest memory written by another writer and how far this host has embedded, so memories that did not come from this host still get embeddings. It adds a table and changes no existing rows. The migration is idempotent.
  • Restart every OpenCode and Pi host on the new version at the same time. This covers OpenCode 1 and OpenCode 2 servers, Pi and OMP processes, and instances in other projects that share the database. After the migration, 0.43.2 and earlier (fence 90) refuse the v91 database the next time they open it. With the default fail_closed_blocking: true, they block the primary-session prompt with a recovery error. Update pinned installs too. Downgrading the plugin does not downgrade the database.
  • OpenCode 1 and OpenCode 2 keep separate plugin caches. If both hosts use the same context.db, update both. doctor now reads the schema fence compiled into every cached Magic Context copy on both hosts. When a copy can't open the current database, doctor fails the check and says how to refresh it (see "doctor" below). Run npx @cortexkit/magic-context@latest doctor --fix after upgrading.
  • OpenCode 2: OpenCode 2 does not install plugin updates on its own. Open /plugins, select Magic Context and press ctrl+u, or run opencode plugin update.
  • Expect one prompt-cache rewrite per session on the first turn after the upgrade. The ctx_reduce guidance in the system prompt changed (see "What the agent sees"). This happens once and does not recur.
  • Rust transform mode: the Rust fixes below are in the ck-mc module and apply after a module update and restart. Its own store (store.db) moves from schema 56 to 60. A ck-mc from an earlier release on a newer store logs a warning and keeps running. This release's ck-mc refuses a store newer than itself instead (MC-C13, see "Rust transform mode" below), so rolling back a later update means restoring ck-mc together with context.db and store.db from the same backup. Rust mode on OpenCode 2 needs both this npm release and the updated module.
  • Rust transform mode on OpenCode 1 and OpenCode 2: historian and dreamer completions now run in the host by default. When historian.runner is unset, they run in the plugin on the models configured for OpenCode and are billed to those provider accounts, instead of being routed to the Broca module. Set historian.runner: "broca" in your user-level config to keep the previous behaviour (see "Rust transform mode" below).
  • History embedding no longer follows memory.enabled. With memory.enabled: false and the default embedding.provider (local), session history is now embedded. Set embedding.provider: "off" if you don't want that (see "Historian and memory" below).
  • The CLI runs on Node 22.16 or later on the 22 line, as well as Node 24 and later. Its engines field is now ^22.16.0 || >=24. Node 23 is not supported.

Fixes for long sessions (#538)

  • A native /compact on OpenCode 1.18 cost two cache rebuilds; it now costs one. OpenCode 1.18 rewrote its compaction prompt, so Magic Context no longer recognised the compaction request as an internal agent. It stored that prompt's hash as the session's own, and the next real turn saw the hash change back and rebuilt the cache a second time. The 1.18 compaction prompt is now recognised, so the request is skipped for injection and hash tracking, as on earlier OpenCode versions.
  • One re-anchor after a native compaction. A native compaction leaves Magic Context's stored history boundary pointing at a message OpenCode no longer loads. The first pass after the compaction now rebuilds the cached history block once, with reason host_compaction, so that the boundary is back inside the loaded window. That pass is already paying for a cache rebuild because of the compaction. The rebuild does not repeat, and which history is rendered does not change.
  • A boundary before the loaded window no longer counts as a refusal on every pass. When a resumed session loads only its most recent messages, the history boundary can sit before the first loaded message. Magic Context logged "absent from current messages" and marked the trim refused on every pass. It now checks where the boundary falls in the session's stored message order. If the boundary comes before every loaded message, there is nothing to cut: that is logged once as boundary-precedes-window. That log line used to say the boundary "sorts before the first live message" and the "whole window" was kept, which read as if OpenCode had not loaded the rows after the boundary. Often it had, and the rows were cut earlier in the same pass with the summarized history, or removed because all their tool calls were dropped. The line now says the boundary "precedes the first remaining message" and that the rows between were cut with the summarized history or removed by reduction. If a boundary inside the window is missing, the whole window is still served, the state is counted, and the next pass that rebuilds the cache anyway moves the boundary to the newest compartment end so the trim finds it again. What is sent to the model is byte-identical to before in all of these cases.
  • The context limit is kept per model. The usage handler raised a session's context limit to the largest input the provider had accepted, without checking which model that input was accepted for. After a model switch, the usage event and the next pass could compute different limits for the same model. The stored floor now carries its model and only applies to that model. Every usage event records what its limit came from (catalog window, a limit learned from an overflow error, the largest accepted input, the model), and the log names the inputs that moved whenever the limit changes.
  • A late usage reading from an older step no longer overwrites a newer one. OpenCode 1 publishes one usage event per step of a tool turn and does not wait for a plugin to finish handling one before it delivers the next. An older step's handler that waited on a model-catalog refresh could finish last and replace the final step's larger prompt size with a smaller one, so pressure stayed too low. Magic Context now keeps the newest assistant message per session and ignores an older reading that arrives after it.
  • On OpenCode 1.18.31 and later, a reading over the model's limit reaches the next pass. These OpenCode versions start the next step's transform without waiting for the usage handler. When a reading was above the catalog limit, the handler refreshed the model catalog before recording the reading, so the next pass still saw the previous, lower pressure and deferred work it should have done. The reading is now recorded first and the refresh runs afterwards. Pi had the same ordering, with a cache alert sent before the reading was saved; the reading is now saved first.
  • The "prepared prompt exceeds engine max_context" error is recognised as a context overflow. Some local inference engines reject an over-long prompt with prepared prompt exceeds engine max_context <N>, or with prompt exceeds ... max_context <N>. Magic Context did not recognise that text, so it did not run its overflow recovery and did not learn the limit. Both forms now count as an overflow, and the number after max_context is recorded as the model's context limit. This applies on OpenCode, Pi and Rust mode (Rust: ck-mc).
  • The message.updated log line names its message and session. It now ends with message.id= and session.id=, so a usage reading in the log can be matched to the message it came from.
  • Log lines are kept when a write to the log file fails. The logger used to discard a batch whose write failed. It now keeps it for the next write, up to 1 MB, and when it has to discard older lines it writes one line saying how many were dropped.
  • A dropped tool result no longer sends its attachments. When a dropped result is kept in place as [dropped §N§] (inside the newest 20 messages, or at the end of the request), files it carried, such as an image from a read, were still sent with it. They are now removed with the output. Results dropped before this release keep their exact bytes, so the prompt cache is not broken for them.

Database locking

  • Writes wait for another process's lock instead of failing (#533, iceteaSA). SQLite starts a transaction as a reader by default. A transaction that reads before it writes must then upgrade to a write lock, and SQLite does not wait for that upgrade: while another process was writing, it failed at once with SQLITE_BUSY, and busy_timeout never applied. Every writing transaction in the OpenCode and Pi plugins now takes its write lock when it begins, so it waits up to the busy timeout like any other write. In Rust transform mode, recording the compaction-marker target after a module pass no longer fails a pass the module already produced: if the lock outlasts the timeout, that pass logs one line and skips the recording, and the next pass records it. A test now fails on any new writing transaction that doesn't take its lock at the start.

OpenCode 2

  • The first request on a large converted session no longer blocks for minutes (#536, null-axiom). The first OpenCode 2 request of a session last served by OpenCode 1 re-derives Magic Context's saved message positions, and it ran inside the request. About 90% of the time went to one tag query per message that could not use an index. That work is now keyed instead of scanned, history is read once instead of twice, statements are reused, and the work yields to the event loop between bounded chunks. It still commits as one transaction. The search-index rebuild that follows runs in 2,000-row transactions and resumes if interrupted. On a 42,534-message session of the reported shape, it went from one 123-second block to 4.8 to 8.0 seconds in total, with no single block over 4.6 seconds. On a real converted session of about 135,000 messages, it went from about 20 minutes in one block to 32 to 70 seconds, with no block over 11.3 seconds.
  • A session over its usage threshold is reduced instead of refused on every turn (#493, MCKLtech). On OpenCode 2, a session whose last provider reading was at 95% or more of the model's window was refused before the request reached the provider. The refusal came before the transform, so queued drops, emergency reclaim, the historian and folds never ran, the reading never changed, and every later turn was refused too. The reported session read 962,842 tokens against a 1,000,000-token model. That check is removed. The transform's emergency path now handles the session as it does on OpenCode 1 and Pi. It refuses with "Context full - /ctx-flush or /clear to continue." only when the provider has rejected the request as too large and the pass could not reduce anything. A turn is still refused before the provider when usage can't be read or recorded.
  • A changed system prompt after a restart is caught on the first pass. On the first pass after an OpenCode 2 restart, Magic Context skipped the system-prompt comparison, because the fallback it used reads OpenCode 1's message table. A changed prompt was then caught one pass late, as a second cache rebuild. The comparison now runs on the first pass, so a changed prompt costs one rebuild.
  • Images read by a tool reach the model as images (#539, DexterHuang). OpenCode 2's read tool returns an image as a file entry next to the text of the result. Magic Context rewrote that result as JSON text, so the model received the base64 data as text instead of an image. The file entries are now kept, so the provider receives the same image blocks OpenCode 2 would send, and their size counts toward context usage. A dropped result loses its images with its output.
  • Sessions are recorded against their project (#542, scookiem). OpenCode 2 gives plugins no SDK client, so Magic Context never recorded which project an OpenCode 2 session belonged to. It now looks the session's directory up once per session through the host, and existing OpenCode 2 sessions are filled in shortly after startup. The Dashboard's Projects page needs this to list OpenCode 2 sessions (see "Dashboard" below).
  • Transform mode rust now runs on OpenCode 2. 0.43.x fell back to the TypeScript transform on OpenCode 2 and showed MC-S06. That code is retired. OpenCode 2 has no compaction row for Magic Context to write, so the module's fold boundary is kept in Magic Context's own store. The host's compaction request is answered from the module's history. Long sessions stay within the module's request limits. The OpenCode 2 adapter no longer copies the full definition of every tool onto every message, which was about 20 KB per message. After a host checkpoint, only rows not yet read are decoded, 100 at a time.
  • A turn refused for storage says why, and later turns recover without a restart (#546, expnn). When OpenCode 2 could not open context.db, for example because another running host still used an older Magic Context build and blocked the migration, every turn was refused with nothing shown in the conversation, and the log said only "context storage is not durable". The refusal now names its cause: the process that blocks the migration (kind and PID), or a database newer than this build, with both schema versions and what to do. A connected Magic Context TUI shows it as a toast on every refused turn. Once per session and cause, a notice starting with [Magic Context storage notice] is stored in the conversation after the refused turn ends. These notices are never sent to the model, and the turn a stored notice starts is ended before the provider. The open is retried at most once every five seconds, so a later turn migrates and goes through once the blocker is gone. Before, every retry found this process's own RPC discovery file and counted the process itself as the blocker. After a recovery, the historian and dreamer start in the running session. Magic Context's tools still need an OpenCode restart, and a one-time notice says so. On every host, the fail-closed message for a blocked migration now states both schema versions, and says that a host loading a pinned or cached older build must be stopped or updated, not only restarted. The OpenCode 1 "disabled" toast names the same cause.
  • execute_threshold_tokens applies on OpenCode 2 (PR #544, claymor333). OpenCode 2 built its scheduler and pressure thresholds from execute_threshold_percentage only, so an absolute token threshold was ignored. With a percentage of 80, a token threshold of 20,000 and 30,000 input tokens on a 100,000-token window, OpenCode 1 executed and OpenCode 2 deferred. OpenCode 2 now reads both, as OpenCode 1 does, once at startup.
  • A hidden child that hit a provider error is replaced (#548, PR #549, claymor333). The historian and dreamer run in hidden child sessions that are reused between runs. After a provider error that ended the run, the child was kept for the next run. It is now stopped and retired, and the next run, including a fallback model's retry of the same run, gets a new child. When the host itself retried a retryable provider error (for example a 500) on a reused child, Magic Context refused the retried step, because the child's history began with an earlier run, and the host logged Failed to drain Session ... hidden_prompt_unrecognized. A later step of a run now starts at that run's own marker, so the host's retry goes through on the same child.
  • A reasoning-only reply that ran out of output budget is reported as that (#551, Littlericket). On OpenCode 2, a hidden historian or dreamer completion that spent its whole output budget on reasoning and returned no text was reported as "Historian returned no assistant output.", because the child's reasoning was not passed back. It now is, and the error says the model ran out of output budget while reasoning and names the setting to raise: historian.maxTokens for the historian, dreamer.maxTokens for dreamer classification and single-reply dreamer tasks, or a model with less reasoning. Measured on a real OpenCode 2 host: an ordinary user turn sends no output cap (max_tokens, max_completion_tokens or max_output_tokens), and a hidden historian request sends none either, so the provider's default applies unless historian.maxTokens or dreamer.maxTokens is set. With historian.maxTokens set, the cap is sent.

doctor and the CLI

  • doctor no longer offers to reconvert an OpenCode 2 store (#493, MCKLtech). When Magic Context's compaction markers were missing from a converted store, 0.43.2 printed a recipe that cleared kv.migration.v1-v2 so OpenCode 2 would convert again. The conversion rebuilds every OpenCode 1 session from its OpenCode 1 rows and deletes that session's OpenCode 2 messages first, so every message added on OpenCode 2 since the first conversion was lost. The recipe is removed from doctor and from the OpenCode 2 docs page. A missing marker is now reported as information: Magic Context on OpenCode 2 keeps its own boundary and doesn't use OpenCode's compaction rows. When OpenCode 2 messages exist that a reconversion would delete, doctor says how many and tells you not to reconvert. If you already followed the recipe, restore opencode.db with its -wal and -shm files from a backup taken before the reconversion.
  • Cached copies that can't open the database are named (#493, deltacat). OpenCode 1 and OpenCode 2 keep separate plugin caches and open the same context.db. Once one host migrates the database, the other host's older cached copy fails closed on every prompt, and an @latest entry does not refresh it. doctor reads the schema fence compiled into every dist-tag copy cached by either host. For each copy that is behind the database, it fails the check with the directory and the fix: for OpenCode 1, quit it and delete that directory; for OpenCode 2, update from /plugins or with opencode plugin update.
  • doctor checks the OpenCode 2 store when OpenCode 1 is first on PATH. With both hosts installed, doctor used to base every store check on the PATH install. It now names both installs and the shared opencode.db. The store, conversion, dangling-boundary and pending-rebase checks run against the OpenCode 2 host, and doctor reads both hosts' plugin logs. Plugin configuration checks still follow the PATH install, and doctor says how to check the other one.
  • doctor warns about historian, dreamer and fallback models the host doesn't list (#542). It used to read the host's model catalog only when a variant was set. It now reads it whenever one of these models is configured and warns when the provider or model is not in the catalog.
  • Row counts that can't be read show n/a (#542). doctor's shared-database row counts showed 0 when a count failed, which looked like an empty table. They now show n/a with the reason.
  • doctor --issue reads the OpenCode 2 log (#542). On an OpenCode 2 host it now reads the OpenCode 2 plugin log first.
  • OpenChamber's bundled OpenCode is found. On macOS, doctor now detects the OpenCode CLI inside OpenChamber.app, in /Applications or ~/Applications.
  • <command> --help prints help for that command. A help flag after a command was ignored before, so doctor --help ran the whole doctor.

Dropped tool calls: real arguments or nothing (#513)

When a tool call inside the newest 20 messages was dropped, Magic Context kept the call and replaced its output. It also replaced the call's arguments with a placeholder such as {"dropped": ...}. Models copied that placeholder into new tool calls. Nothing is written in place of arguments now:

  • a dropped call whose text arguments come to 1 KB or less keeps its real arguments, and its output is replaced by [dropped §N§];
  • a larger call is removed with its output, as drops outside that window already were;
  • the call whose result ends the request, and calls next to the model's reasoning, keep their real arguments whatever their size.

This applies on OpenCode, Pi and Rust mode (Rust: ck-mc). Placeholders already served in a session keep their exact bytes, so the prompt cache is not broken for them. They are replaced with real arguments on the next pass that rebuilds the cache anyway.

Fewer cache rebuilds

  • A history rebuild that produces identical bytes no longer lets other changes through. Some rebuilds of the cached history block reproduce it byte for byte, for example after a project memory update that changes nothing rendered. The provider cache survives such a pass. It still let queued ctx_reduce drops, age-based reclaim, heuristic cleanup and the synthetic todo reminder apply, so one of them became the pass's only cache break. They now apply only when the rebuild changes the bytes already sent, or loses the provider cache anyway (a model change, a changed system prompt, an idle expiry). The memory mural counts as part of those bytes, including an image appearing or disappearing, and a stored mural that hadn't been loaded yet is loaded before the comparison. This applies on OpenCode, Pi and Rust mode (Rust: ck-mc).

Queued drops after a long idle (#545, tenshiak)

  • Queued drops apply on the first turn after an idle longer than the cache TTL. Magic Context measures how long the provider cache has been idle from the last provider response. That time was also reset by messages that were not provider responses: on Pi, the user's own new prompt, which Pi records just before the context pass, as well as tool results and failed requests; on OpenCode 1, the empty assistant message OpenCode creates for a new request. So the first turn after a long pause saw an idle of a few milliseconds, deferred, and left queued ctx_reduce drops pending, logging pending ops WILL NOT APPLY - reason=scheduler_defer. Only a response with provider usage now resets the idle time, on Pi and OpenCode 1. On OpenCode 2 a stored reply with no usage no longer resets it either.

Usage readings (#534, tenshiak)

  • Pi: the whole-branch estimate after a retry is no longer used as usage. After a failed request, Pi removes the failed attempt and then reports a character-count estimate of the whole unreduced session branch as context usage, until the provider reports again. In the report, this showed 470K on a model with a 272K window, and it started an emergency drop. Magic Context now recognises that estimate with Pi's own rule (a context edit or compaction newer than the last provider usage) and keeps the previous reading, at any size. Usage the provider reports for a completed request still counts in full on every host, even above the configured window, because a configured window can be smaller than what the model accepts. That reading counts as pressure over 100%, but it does not raise the session's learned limit.

Rust transform mode (ck-mc)

  • Conditional notes work in Rust mode (#535, iceteaSA). The host never told the module that note conditions could be evaluated, so every ctx_note write with a surface_condition was refused. The host now sends that capability. Adding a condition to an existing session note, which no evaluator handles and which left the note hidden as pending, is now refused with a message saying to write a new note with the condition. Session notes already stuck as pending are restored when the store opens. (npm package and ck-mc.)
  • Fewer mass re-encodes after a degraded store. When the module's store was degraded and dropped its raw message trees, synthetic reminder messages lost part of their cache key. Every served message from that point on was re-encoded on the next pass, and the same range again on the pass after. Keys of unchanged messages now stay the same.
  • /ctx-wrapup waits long enough for fallback models. Each wrapup round waited a fixed 10 minutes. It now allows the historian timeout for each model in the fallback chain, within the request's overall budget.
  • Historian and dreamer run in the host by default on OpenCode 1 and OpenCode 2. The new historian.runner setting chooses who runs historian completions: "host" runs them in the plugin on its configured historian model, "broca" routes them to the Broca module. When it is unset, OpenCode 1 and OpenCode 2 use "host", and Claude Code through the Thalamus gateway, which has no host to run a completion, uses "broca". A new dreamer.runner setting does the same for the dreamer completions the module routes (memory classification); when unset it follows historian.runner. Both settings are honoured only in user-level config, since they decide whose provider account pays. In Rust mode, the OpenCode status view shows which runner each one uses and whether it was configured or is the default. (npm package and ck-mc.)
  • The historian checks the prompt against every fallback model's window (#541, iceteaSA). The module size-checked a historian prompt only when historian.context_limit_tokens was set, and only for the first model. The plugin now sends each model's context window and output limit for the whole fallback chain, and the prompt is checked against the model that will receive it, in the module and in the host runner. A prompt too large for a fallback model is refused instead of sent. A model whose window is unknown is still sent the prompt, and that is logged once. (npm package and ck-mc.)
  • ck-mc refuses a store.db newer than itself (#540, iceteaSA). An older ck-mc started on a store that a newer build had migrated logged a warning and kept running. It then half-worked: since store migration 53, note ownership is enforced by triggers that read a row an older binary never fills, so note inserts and deletes failed later without naming the version skew. ck-mc now refuses to open such a store before it reads or writes anything. ck health magic-context reports failing with storage_state: open_refused_store_ahead and both versions. A turn is refused with MC-C13: "Magic Context refused to start: its store (store.db) is at schema vN but this ck-mc build only knows up to vM. Update ck-mc, or roll back by restoring ck-mc together with context.db and store.db from the same backup." The turn is not answered from the last good result, not sent unmanaged and not parked, so every later turn asks the module again and refuses visibly until the store and binary match. ctx_memory and ctx_note reply with the same sentence. Before, when the module's ownership check failed on a project without the module's ownership marker, ctx_memory and ctx_note wrote through the host instead. With compaction off, the refusal appears only in the log, as other fail-closed refusals do. A store older than the binary still migrates forward on open. (npm package and ck-mc.)
  • Diagnostics: the module's health report now includes its render epochs and build SHA, and store warnings go to the module's log instead of stderr.
  • Groundwork for a future single-database mode ships switched off and changes nothing at runtime.

Historian and memory

  • History embedding runs whenever an embedding provider is configured (#543, null-axiom). Embedding of session history, and semantic ctx_search and auto-search over it, used to stop when memory.enabled was false. They now run whenever embedding.provider is not off, on OpenCode, Pi and /ctx-embed. memory.enabled: false still turns off memories, promotion, the dreamer's memory tasks and memory injection, and memories are never embedded while it is off.
  • Facts under an unknown category are reported (#537, G0-0000). The historian's facts are grouped under five categories. Facts under any other tag, for example a misspelt one, were dropped without a trace. They are still not stored, but the log now names the category and how many facts it held. This applies in TypeScript and Rust mode (Rust: ck-mc).

Anthropic thinking-block recovery

  • Recovery covers Claude Opus 5.5 and needs one failed request. On Claude Fable 5.1 and Claude Opus 5.5, Anthropic rejects a request when anything before a thinking block has changed since it was produced, which a Magic Context edit to the start of the conversation can cause. Recovery was limited to Fable 5.1, and it removed one assistant message's reasoning per failed request, starting from the newest, while the error names the oldest failing block. After one edit, a session could fail once for every assistant message with reasoning still in the request. After one rejection, recovery now removes the reasoning from every such message in the request, so the next request succeeds, and later passes repeat the same removals byte for byte. This applies to the anthropic provider on OpenCode (TypeScript and Rust mode) and Pi.
  • A pass that already rebuilds the cache removes the thinking it invalidates. Recovery acts only after a request has been rejected. A pass that rebuilds the cache changes bytes before existing thinking blocks, and the provider then drops those blocks, or on newer accounts rejects the request. On Claude Fable 5.1 and Claude Opus 5.5 with the anthropic provider, such a pass now removes the reasoning of every assistant message still in the request itself. This includes an unfinished tool round, whose tool call is kept: a live test found both models accept that turn with its thinking removed. The removed messages are recorded before the request is sent, and every later pass removes the same blocks, so a pass that does not rebuild the cache sends the same bytes as the one before it. If the record can't be written, nothing is removed on that pass and recovery remains the fallback. Reasoning produced afterwards is kept until the next pass that rebuilds the cache. Last-known-good replays in TypeScript and Rust mode also remove the recorded thinking from the messages they replay. Subagent sessions and other models are unchanged.
    • Rust mode: a pass that only releases a frozen last-known-good replay removes reasoning from the first message that changed onward, not from the whole request. When it can't tell that the stored replay is what was last sent, it starts at the first change inside the replay or at its end. Empty text and reasoning parts, which OpenCode drops before sending, don't count as a change.
    • Pi removes thinking at the end of the context pass instead of the start, so every stage of the pass sees the same thinking on every pass. A session that already has removals from an earlier build keeps the start order, without the new removal, until a pass that is allowed to rebuild the cache anyway switches it.

What the agent sees

  • ctx_reduce reminders say to stamp outputs as soon as they are used. The reminders used to say "at a natural stopping point". They now tell the agent to stamp outputs it has already used right away, for example "Your next step: call ctx_reduce on the outputs you've already used". The guidance in the system prompt says an output counts as used once the agent has taken what it needs from it, and that a reminder means stamping is overdue. TypeScript, Pi and Rust render the same text. Reminders already in a session keep their bytes; new reminders use the new wording.

Dreamer

  • Worktrees sharing a project no longer postpone each other's tasks (#498, DivyamTalwar; PR #532, Oscar-Williams). When worktrees of one project had different dreamer schedules, each one reset the shared next-due time to its own, so a task could be pushed back again and again. A schedule change now keeps the earlier of the new slot and the slot already held, and keeps the retry count when that slot is kept. A worktree with the task disabled no longer changes the shared schedule. Reconciliation runs in one write transaction, so two processes can't interleave. During a daylight-saving fall-back hour, a task that already ran at the first occurrence of the repeated local time is not scheduled again for the second.
  • Dreamer tasks on OpenCode 1 no longer run into the host's request timer. Some OpenCode 1 builds give plugins an SDK client whose requests keep Bun's default timer, which fires 300 to 360 seconds after a request starts. Verify, map-memories, classify and compress-cues sent each prompt as one request that stayed open until the child's whole agent loop ended, so a long batch failed with TimeoutError ("The operation timed out.") while the child kept running. That error counted as a provider error, so the next model in the chain was sent into the same busy child and met the same timer, which is why these failures usually show a duration of about 12 minutes in the ledger. These tasks, and the smart-note confirmation run, now send without waiting for the reply and poll until the child is idle, under the task's own time limit. That limit is the only timer, and it still stops the child when it fires. A client without asynchronous sending keeps the previous request. The historian is unchanged.
  • A host TimeoutError is a timeout. It is no longer retried on the next model. It is recorded as provider_timeout with status timed_out, counts toward map-memories' stop after repeated timeouts, and stops the child's run.
  • Verify batches get at least four minutes. Verify split the task's time evenly across its 50-memory batches with no minimum, so a project with many memories to verify gave each batch too little time (120 seconds each for ten batches in 20 minutes) and batches timed out. Each batch now gets at least 240 seconds, the minimum map-memories already had, and a batch is not started when less time remains; its memories are picked up by a later run. When a batch times out, the batches before it stay saved and the run stops, because every later batch would get the same time or less. That run is recorded as failed, but it is not retried straight away: the task waits for its next scheduled run.
  • Failed runs record their tokens and model. On OpenCode 1, a failed verify, map-memories or classify row in the ledger now carries the child session's token totals and the model that answered, as completed rows and Pi's rows already did. The child's messages are read for at most 5 seconds before it is removed.
  • When every model fails, the error names the last failure. The error used to be the first model's failure. It now reads All models exhausted (<models tried>): <last error>.
  • A dreamer task is no longer stopped because its lease expired while nobody else took it. A running task renews its lease every 60 seconds, and the lease lasts 120 seconds. When a renewal found the lease expired, the task was treated as having lost it and its current prompt was stopped, even when no other process had taken it. A machine sleeping for more than two minutes, one renewal that failed on a busy database, or a long gap between two tasks of the same group was enough. The task now takes back an expired lease that still names it with the same generation and keeps running. It stops only when another holder or a new generation is confirmed. A renewal that fails on a busy database is tried again at the next renewal. Renewals now run every 40 seconds, and the lease is renewed at the start of each task in a group. When a task does lose its lease, the ledger's error gives the reason (lease_lost: taken by <holder>, lease_lost: generation changed or lease_expired: reacquire failed (holder missing)) instead of only "prompt aborted by external signal".
  • Hosts that don't own a project's memories no longer spend model calls on them. When a project's memories are managed by the Rust module, because a Rust-mode OpenCode took them over, a host with no route to the module (Pi, an OpenCode in TypeScript mode, or an older process) still ran its memory tasks. Classify, for example, ran its whole model call and then failed when writing the result. The memory tasks (curate, map-memories, compress-cues, classify-memories, verify, verify-broad and retrospective) now check ownership first and skip the project, logging it once. The smart-note sweep also skips such a project. The smart-note sweep runs before the scheduled tasks, and a refused note write there used to stop every dreamer task for that project on that host; a failure in the sweep is now logged and the tasks still run.
  • Classify sends only memories that aren't classified yet. For a project with 10 to 100 memories, classify re-classified all of them on every run. It now sends only memories that are not classified yet, as it already did above 100.
  • Classify through the Rust module allows time for the whole fallback chain. The request allowed 660 seconds, enough for one model, so a fallback model's attempt could be cut off. It now allows 660 seconds for each model in the chain, plus 30 seconds.
  • A hidden dreamer agent that reaches its step limit is reported as that (#550, morpheus9393). On OpenCode 2, a dreamer task whose hidden agent went over its step limit was reported as a provider error and retried on the next model. It is now recorded with failure class step_limit and reported as MC-D10: "Memory maintenance stopped at its hidden agent step limit. This task needs less work per run; changing the model connection will not help." It is not retried on another model, and the child session is retired. A hidden run that fails without a provider error is no longer reported as one either.

Pi

  • TypeBox stays a bundled dependency (#525, trim21). #525 asked for typebox to be a peer dependency. As an external import, OMP's extension loader replaces typebox with its own shim, whose schemas fail tool registration, so Magic Context would stop loading on OMP. It stays a regular dependency bundled into the package, and a test guards this.
  • Tool errors are reported as errors (#547, tenshiak). Magic Context's Pi tools, such as ctx_reduce, returned a failure as a result marked isError. Pi's agent loop ignores that mark, so a failed call was recorded as successful. A failure is now raised as an error with the same text, and Pi records the tool result as failed.
  • The dreamer and historian drop models Pi can't resolve. Models in the dreamer and historian chains are now checked against Pi's own model registry. A model Pi doesn't know, for example an OpenCode provider id, used to be tried on every batch and fail with Pi's Model "..." not found. It is now left out of the chain, with one warning in the log. A dreamer task with no model left is skipped, the historian with no model left doesn't run, and /ctx-status shows a warning naming them.
  • No dreaming in / or the home directory. A Pi session started in the filesystem root or in the home directory no longer registers that directory with the dreamer.
  • @cortexkit/subc-client in the Pi plugin and the CLI is now 0.11.1, the version the OpenCode plugin already uses.

Dashboard

These changes are in the next Dashboard release, which ships separately from the plugin.

  • The Projects page lists OpenCode 2 sessions (#542, scookiem). Every OpenCode store query read OpenCode 1's session table, which a fresh OpenCode 2 store does not have, so the page showed "No projects yet". The Dashboard now reads OpenCode 2's session table. Grouping by project also needs this plugin release, which records each OpenCode 2 session's project.
  • The Logs page reads the OpenCode 2 plugin log (#542). It read only the OpenCode 1, Pi and OMP logs before.
  • The Cache tab updates during a live OpenCode 2 turn (PR #544, claymor333). OpenCode 2 updates a session's time when a prompt is sent and not again until the next prompt, so the Cache tab showed nothing new for the rest of the turn. It now also reads the time of the session's newest message. On an OpenCode 2 store older than 2.0.3, which lacks that time, it falls back to the session's time and says so above the cards.
  • The "OpenCode 2" filter works in the Cache tab and the Sessions viewer (PR #544, claymor333). The Cache tab's filter listed nothing, because the OpenCode store was loaded only for the "OpenCode" filter. The Sessions viewer did not offer the filter at all, and a saved "OpenCode 2" filter fell back to "All".
  • The Logs page reads message.id and session.id on message.updated lines as fields (#538).

Credits

Thanks to Sugaroverdose (#538), iceteaSA (#533, #535, #540, #541), tenshiak (#534, #545, #547), null-axiom (#536, #543), MCKLtech and deltacat (#493), trim21 (#525), DivyamTalwar (#498), G0-0000 (#537), DexterHuang (#539), scookiem (#542), expnn (#546), claymor333 (#548), morpheus9393 (#550) and Littlericket (#551) for the reports, and to Oscar-Williams for PR #532 and claymor333 for PRs #544 and #549.