<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Antonio Zhu</title>
    <description>The latest articles on DEV Community by Antonio Zhu (@antonio_zhu_e726fd856cd86).</description>
    <link>https://dev.to/antonio_zhu_e726fd856cd86</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2014487%2Fd84666c3-05b7-44c6-aa55-ccd142ad4b24.png</url>
      <title>DEV Community: Antonio Zhu</title>
      <link>https://dev.to/antonio_zhu_e726fd856cd86</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/antonio_zhu_e726fd856cd86"/>
    <language>en</language>
    <item>
      <title>An AI-Assisted Development Methodology, Earned from Real Incidents</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:26:05 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/an-ai-assisted-development-methodology-earned-from-real-incidents-1gik</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/an-ai-assisted-development-methodology-earned-from-real-incidents-1gik</guid>
      <description>&lt;p&gt;This document is a methodology for building software with AI assistance, compiled from the rules that survived real use on a production data platform. It is not theory. Every rule in here exists because a real incident forced it into existence, and every case study at the end is a bug or a mistake that actually happened. What I write below is what I learned — translated from the internal playbook, with company and vendor names removed.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Core Principles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why a methodology at all
&lt;/h3&gt;

&lt;p&gt;AI writes code fast, but fast is not the same as correct. AI reliably produces content that &lt;em&gt;looks&lt;/em&gt; right and &lt;em&gt;is&lt;/em&gt; wrong: SQL with the right syntax but a misunderstood definition, a change in one place that silently breaks a consumer somewhere else, a root cause left unexamined under the phrase "the difference is within acceptable range." The purpose of a methodology is to keep AI's output inside a boundary where it can be verified, traced, and maintained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single source of truth
&lt;/h3&gt;

&lt;p&gt;Every kind of information has exactly one authoritative location. Everything else references it, never copies it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entity definitions, fields, sync frequency → the entity config files&lt;/li&gt;
&lt;li&gt;Warehouse table structures and refresh functions → the migration files&lt;/li&gt;
&lt;li&gt;Warehouse dependency relationships → the job config files&lt;/li&gt;
&lt;li&gt;BI card SQL and dashboard layout → the IaC scripts&lt;/li&gt;
&lt;li&gt;Business definitions → the per-dashboard guide documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the truth changes, only the authoritative source changes. Every other document is just an index pointing at it.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Safety Red Lines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The production database is read-only
&lt;/h3&gt;

&lt;p&gt;Every access path into the production databases — the ERP and the warehouse — allows read-only queries. Forbidden: &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;MERGE&lt;/code&gt;, &lt;code&gt;BACKUP&lt;/code&gt;, &lt;code&gt;RESTORE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;DROP&lt;/code&gt;, &lt;code&gt;ALTER&lt;/code&gt;, regardless of whether it is wrapped in a transaction that will be rolled back, and regardless of whether the target is &lt;code&gt;NUL&lt;/code&gt; or a temp table.&lt;/p&gt;

&lt;p&gt;This red line was actually violated once. While exploring the ERP's budget module, an AI tried to "verify whether the account had write permission" by executing &lt;code&gt;UPDATE ... WHERE 1=0; ROLLBACK&lt;/code&gt; and &lt;code&gt;BACKUP DATABASE xxx TO DISK='NUL'&lt;/code&gt;. Even though no data was damaged, the operation itself produced a write intent and lock contention against a production database. That is an unacceptable risk. It was stopped on the spot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; the only way to verify a permission boundary is through read-only catalog queries (role membership, database permissions). You cannot verify it by "actually executing a write and seeing whether it errors." "It looks harmless" is not an exemption.&lt;/p&gt;

&lt;h3&gt;
  
  
  The red line must cover "the tool objectively exists", not just "the tool description says read-only"
&lt;/h3&gt;

&lt;p&gt;This rule went through an important wording correction, and the process of correcting it matters more than the conclusion. The first version of the red line said "the ERP MCP's query tools are restricted to read-only." That sentence has a hidden hole: it only describes the AI's &lt;em&gt;belief&lt;/em&gt; about the tools, while the system actually registers dozens of genuinely callable write tools — &lt;code&gt;save_bill&lt;/code&gt;, &lt;code&gt;audit_bills&lt;/code&gt;, &lt;code&gt;submit_bills&lt;/code&gt;, &lt;code&gt;push_bill&lt;/code&gt;, &lt;code&gt;delete_bills&lt;/code&gt;, &lt;code&gt;cancel_bills&lt;/code&gt;, &lt;code&gt;void_bills&lt;/code&gt;, &lt;code&gt;close_bill&lt;/code&gt;, &lt;code&gt;forbid_bills&lt;/code&gt;, &lt;code&gt;create_and_audit&lt;/code&gt;, &lt;code&gt;workflow_approve&lt;/code&gt;, and more. These are not "raw API calls that bypass the restriction." They are real, registered tools sitting in the tool list, callable by name.&lt;/p&gt;

&lt;p&gt;An independent review found this hole: if you only say "restricted to read-only," it is easy for a person (or an AI) to believe "the system has no write tools." The truth is "the write tools objectively exist, and the business rule requires a human not to call them." Those are two completely different levels of protection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; a red line must enumerate tools exhaustively by name prefix or action keyword, not cover an entire class of tools with one summarizing sentence. The project's current red line section lists every tool name individually. That is not verbosity. It is necessary precision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The stronger next step:&lt;/strong&gt; a pure-text red line relies on the AI actively mapping "tool name → red line description" every single time, which is fragile. A more reliable implementation is a plugin-level &lt;code&gt;tool.execute.before&lt;/code&gt; hook that hard-blocks the ERP write tools by name prefix (&lt;code&gt;save_*&lt;/code&gt;, &lt;code&gt;audit_*&lt;/code&gt;, &lt;code&gt;submit_*&lt;/code&gt;, &lt;code&gt;push_*&lt;/code&gt;, &lt;code&gt;delete_*&lt;/code&gt;, &lt;code&gt;cancel_*&lt;/code&gt;, &lt;code&gt;void_*&lt;/code&gt;, &lt;code&gt;close_*&lt;/code&gt;, &lt;code&gt;forbid_*&lt;/code&gt;, &lt;code&gt;enable_*&lt;/code&gt;, plus &lt;code&gt;create_and_audit&lt;/code&gt; and &lt;code&gt;workflow_approve&lt;/code&gt;), turning "the business rule" into "objectively impossible to execute" rather than leaving it in the text-constraint layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The production server operation rule
&lt;/h3&gt;

&lt;p&gt;Read-only diagnostics only (&lt;code&gt;docker ps&lt;/code&gt;/&lt;code&gt;logs&lt;/code&gt;, &lt;code&gt;df&lt;/code&gt;, &lt;code&gt;systemctl status&lt;/code&gt;, &lt;code&gt;pg_isready&lt;/code&gt;, health-check curl). Forbidden: any imperative change — removing/stopping/rebuilding containers, dropping volumes, &lt;code&gt;rm -rf&lt;/code&gt;, &lt;code&gt;DROP&lt;/code&gt;, &lt;code&gt;kill&lt;/code&gt;, &lt;code&gt;systemctl stop/disable&lt;/code&gt;, editing config by hand. Every production change goes through IaC + CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why these red lines are written down instead of remembered
&lt;/h3&gt;

&lt;p&gt;An AI has no "this should be safe" intuition circuit breaker. A human feels a vague tension before a high-risk operation, and that tension makes them think twice. An AI does not. It will execute a normal query and a &lt;code&gt;DROP TABLE&lt;/code&gt; with the same calm tone. That is why the red lines must be written out explicitly, down to the specific tool names — never left to "the AI should know this is dangerous."&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Development Tools — OpenCode and Its Extensions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why OpenCode
&lt;/h3&gt;

&lt;p&gt;The project uses OpenCode as its day-to-day terminal agent. There are three reasons, the first two about selection and the third an honest assessment of its limits discovered after use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Model-switching flexibility.&lt;/strong&gt; OpenCode is vendor-neutral. A single config file can hold multiple providers side by side (this project runs both Anthropic and DeepSeek), and switching between Claude and DeepSeek models is just a change in the conversation window. In practice a clear division of labor emerged: &lt;strong&gt;design work uses the Anthropic (Claude) model, implementation uses the DeepSeek model.&lt;/strong&gt; Work that needs complex reasoning, architecture design, trade-off analysis, or definition judgment goes to the former, because those tasks demand more reasoning depth and judgment; concrete coding and relatively mechanical implementation switch to the latter, because implementation has a lower marginal cost and needs less reasoning depth. Different tasks demand different levels of model capability and cost. Locking into a single vendor's toolchain throws away the freedom to use the strong model when you need strength and the cheap model when you need economy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open source, avoiding the trust problem of closed clients.&lt;/strong&gt; OpenCode is open source and its code is auditable. There is none of the "I can't see what data it sends where, or whether there is an opaque backdoor" worry that comes with closed agent clients. For a workflow that touches production databases and an ERP system — sensitive data sources — running on a code-visible, behavior-auditable client is the basic prerequisite for lowering supply-chain risk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not the strongest on its own, but the gap is filled by extensions.&lt;/strong&gt; Honestly, OpenCode's built-in capabilities (the agent framework layer, not the model) trail some more commercially polished products. But most of that gap can be closed through its open MCP protocol and plugin mechanism. The MCP servers and plugins described below cover the large majority of real daily needs. So the reason is not that OpenCode is better at everything; it is that its open architecture buys room to customize and strengthen against this specific project, and that room has more long-term value than "a bit more built-in capability."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Three data-link layers, three MCP servers
&lt;/h3&gt;

&lt;p&gt;Structured tool calls like MCP give every high-risk operation a chance to be intercepted or audited at the tool-name level. Free-text command assembly (raw &lt;code&gt;curl&lt;/code&gt;, raw &lt;code&gt;psql -c&lt;/code&gt;) bypasses that protection and is the classic entry point for "raw calls that bypass the restriction" — a path this project's red lines explicitly close off.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The ERP MCP:&lt;/strong&gt; the source system, with 50+ tools covering document queries (purchase/sales/stock-in-out/outsourcing/production/asset), instant inventory, material/customer/supplier master data, general-ledger reports, approval flows, and audit logs. Query tools (names containing &lt;code&gt;query&lt;/code&gt;/&lt;code&gt;view&lt;/code&gt;/&lt;code&gt;list&lt;/code&gt;/&lt;code&gt;get_fields&lt;/code&gt;/&lt;code&gt;validate&lt;/code&gt;/&lt;code&gt;discover&lt;/code&gt;/&lt;code&gt;describe&lt;/code&gt;) and write tools are clearly separated by name prefix — the naming convention itself is part of the safety design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pg-schema-semantic + pg MCPs:&lt;/strong&gt; postgresql warehouse-structure exploration (&lt;code&gt;list_tables&lt;/code&gt;/&lt;code&gt;describe_table&lt;/code&gt;/&lt;code&gt;sample_data&lt;/code&gt;/&lt;code&gt;search_schema&lt;/code&gt;) and read-only SQL queries are split cleanly — the former sees structure, the latter runs arbitrary &lt;code&gt;SELECT&lt;/code&gt; for data exploration and reconciliation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Metabase BI MCP:&lt;/strong&gt; the full lifecycle of BI cards and dashboards, from the read-only &lt;code&gt;get_card&lt;/code&gt;/&lt;code&gt;execute_card&lt;/code&gt; to the mutating &lt;code&gt;create_card&lt;/code&gt;/&lt;code&gt;update_dashcard&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tooling gap: no general web search
&lt;/h3&gt;

&lt;p&gt;Recent sessions looking up an ERP report form ID hit the same limitation twice: there is no structured web-search MCP tool, only &lt;code&gt;webfetch&lt;/code&gt; for single-URL fetching. Both times the agent was forced to feed search-engine result-page URLs into &lt;code&gt;webfetch&lt;/code&gt; as a proxy for searching, which exposed specific problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bing has terrible recall for long-tail Chinese technical terms, returning almost nothing but homepage noise.&lt;/li&gt;
&lt;li&gt;Google's search page requires JS to redirect; &lt;code&gt;webfetch&lt;/code&gt; can't execute JS, so it gets nothing real.&lt;/li&gt;
&lt;li&gt;GitHub's &lt;code&gt;is:open&lt;/code&gt; filter silently fails when fetched through &lt;code&gt;webfetch&lt;/code&gt;'s raw GET — the result set mixes in many actually-closed issues, so the status field has to be re-verified by hand; never trust a filter declared in the URL query string.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The implication:&lt;/strong&gt; this kind of external information retrieval (researching technology trends, looking up third-party API docs) is complementary to, not a substitute for, the project-internal data tools. The gap is real. A search MCP that returns a structured result list is more reliable than making the AI hammer a search engine through &lt;code&gt;webfetch&lt;/code&gt;; and for any page that depends on client-side JS rendering or a platform filter parameter, the results must be re-verified — never assume a filter declared in the URL has already taken effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  The OpenCode plugin layer
&lt;/h3&gt;

&lt;p&gt;MCP tools extend "which external systems can be connected, which business data can be queried." OpenCode's plugin mechanism extends a different category — &lt;strong&gt;infrastructure problems in how the agent itself runs&lt;/strong&gt;: how context is managed, how environment variables are injected, how long-running commands are awaited, how memory persists across sessions. The two are not interchangeable. The former determines what data the AI can see; the latter determines whether the AI's session itself "lives well." The project has five plugins installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;wait_for&lt;/code&gt; (polling await):&lt;/strong&gt; turns the "hand-write a &lt;code&gt;for&lt;/code&gt; loop that repeatedly &lt;code&gt;sleep&lt;/code&gt;s and &lt;code&gt;curl&lt;/code&gt;s to check whether a service is ready" pattern into one tool call, supporting HTTP/TCP/shell command targets. It maps to the recurring scenario in the "data refresh pain" section below — after changing code you have to wait for some external link (deployment taking effect, service restart, port becoming connectable) before continuing. That wait should be a standard tool expressing "wait until ready," not an ad-hoc polling loop reinvented every time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;direnv&lt;/code&gt; (automatic environment loading):&lt;/strong&gt; loads the project's environment-variable config automatically at session start, so you don't have to &lt;code&gt;source&lt;/code&gt; it by hand every time. It maps directly to a real stumble mentioned later — a Go program reading variables with &lt;code&gt;os.Getenv&lt;/code&gt; does not auto-load the &lt;code&gt;.env&lt;/code&gt; file, so it must be injected manually before anything runs. That class of "looks like a logic bug, is actually a missing env var" blockage is cleared up-front by this plugin, cutting out the wasted debugging of "code changed but verification failed because the env var never arrived."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;compress&lt;/code&gt; (dynamic context pruning):&lt;/strong&gt; the core ability is replacing conversation content that is already handled and no longer needs verbatim retention with a high-fidelity summary, instead of waiting for a static compaction when the context hits the ceiling. It does two supporting things: it deduplicates repeated tool calls (same tool, same arguments) keeping only the latest output; and after an errored tool call, a few turns later, it prunes the possibly-large original input while keeping the error message itself. All three share one goal — decide "does this still need to be kept verbatim" by understanding the content, not by mechanically truncating by position. This is the technical base on which the "context management" discussion below actually runs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;memory&lt;/code&gt; (cross-session persistence):&lt;/strong&gt; plain Markdown storage, no background consolidation model, no cloud dependency — what to store and when is decided entirely by the AI in the current session, and a compact summary is injected into every session's system prompt. This is the mechanism underneath the entire "memory and context management" chapter, and the direct answer to the architectural defect that "models have no cross-session memory."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;session_reflection&lt;/code&gt; (qualitative session review):&lt;/strong&gt; provides collecting a session's content for retrospective analysis, saving a reflection report, and inspecting the current session's system-prompt composition. It turns "how did this collaboration go, and what can be improved" into something that can be structurally inspected instead of recalled by impression.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What these five share:&lt;/strong&gt; they take "the weak points where AI collaboration itself repeatedly goes wrong" and turn them from "frantically patched ad-hoc in every session" into "infrastructure handled by default by the environment." Awaiting external links, reading env vars, managing a bloated context, remembering lessons across sessions — if all of these had to be solved ad-hoc by the AI in every session, it would be slow and would reproduce the anti-patterns discussed earlier (fabricated data to bypass verification, context dilution degrading judgment). Pushing these down into the plugin layer raises the "passing grade" of AI collaboration one level, so every session starts from a more reliable base instead of re-fighting the same infrastructure friction.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Workflow Stage Rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Discuss before implementing
&lt;/h3&gt;

&lt;p&gt;Even when the user gives detailed implementation instructions, discuss the approach, trade-offs, and design first. Write no code until the user explicitly says "go ahead." The reason: detailed instructions usually carry architecture choices the user has not consciously noticed, and the discussion stage is the only window to surface those hidden assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The most common misjudgment here is "the user already described it in so much detail, what is there to discuss."&lt;/strong&gt; Recent sessions repeatedly proved the cost of that misjudgment. The more detailed the instruction, the more it tempts a person (or an AI) to skip the discussion and start. But detailed is not the same as complete. Detail describes the half the user has already thought through; the half they haven't is precisely the part hiding in the gaps of that detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A "hidden architecture decision" case:&lt;/strong&gt; a user gave an instruction that looked like it needed no discussion at all — "I want the pure not-yet-stocked-in definition, and update the requirement doc first." The instruction named exactly one metric to change: the wafer-in-process count. Before acting, the AI realized: the "wafer resource pool" metric is defined as "in-process + in-stock," and its in-process component uses the &lt;em&gt;same&lt;/em&gt; calculation. Change only the in-process count and leave the pool alone, and the two metrics become logically contradictory (the child uses the new definition, the parent still uses the old one, and they no longer add up). So the AI did not silently decide "change only what the user named" or "change the related one too." It laid the linkage out and asked: "Should the resource pool change along with it?" Only after the user confirmed "yes, change it too" did the work start.&lt;/p&gt;

&lt;p&gt;The value of this case: the instruction was specific down to "which definition to change," which looks like there is no discussion space left — and yet the AI still found a downstream surface the user hadn't mentioned but was logically and necessarily related. "Instruction is detailed" and "instruction is complete" are different things. Detail measures how much the user expressed; completeness measures how many hidden connection points the system requires to be considered together. The former, no matter how high, can never substitute for checking the latter.&lt;/p&gt;

&lt;p&gt;A similar case: a user supplied a mapping table for a new dimension that looked complete and thorough. The AI did not take this "detailed data" and immediately write the migration. It first queried all real values of that field in the database, found the mapping table left several real values uncovered (including some non-standard and NULL values), asked how those uncovered values should be classified, and only then wrote the migration. The hidden assumption here was "the list the user gave covers every case" — which is almost never true in complex data scenarios, and the cost of checking is far lower than the cost of discovering later that uncovered values were silently miscategorized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reverse inference:&lt;/strong&gt; if the AI had taken "change the in-process count to the pure not-yet-stocked-in definition" and started immediately, changing only that one metric, the user would later find the parent-child definition mismatch in the resource pool. That rework costs far more than asking one question up front — and this kind of inconsistency usually does not error immediately; it may only surface during a later data audit, when the investigation cost is even higher. That is what "the discussion stage is the only window for hidden assumptions" actually means: once the window closes (code changed, already deployed), the same assumption error costs an order of magnitude more to find and fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  The circuit breaker
&lt;/h3&gt;

&lt;p&gt;In an ongoing session, when a new request hits any of the following signals, stop and do not take it on directly: it needs a new file/module or lands in a different top-level directory, it introduces a new tech stack, or it has no direct dependency on the current task. When hit, do four things: name it as a new scope, report the current account (what this session has already done), suggest opening a new session, and wait for the user to decide.&lt;/p&gt;

&lt;p&gt;The reason this exists is blunt: one runaway session ($60, 7 hours) rolled up this way — not because any single step was wrong, but because nobody ever stopped at an expansion point to do the accounting. The circuit breaker is not a refusal of new requests. It is forcing an explicit confirmation at the expansion point.&lt;/p&gt;

&lt;h3&gt;
  
  
  One session, one task
&lt;/h3&gt;

&lt;p&gt;When the user raises a clearly unrelated new topic mid-session, remind them: "this is a new task; the current session is still on X — open a new session?" This does not apply to sub-agent delegation, closely-related sub-tasks of the same feature, or the user explicitly continuing the current topic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core problem this rule solves: packing too many scattered, independent tasks into one session systematically degrades the LLM's performance.&lt;/strong&gt; This is not a style preference; it is a direct consequence of the context mechanism, in three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The context window is a finite resource, and multiple tasks crowd each other out.&lt;/strong&gt; This environment's compression mechanism (the &lt;code&gt;compress&lt;/code&gt; tool) replaces older content with a summary as the context approaches its ceiling, and that summary is lossy — it keeps structural conclusions but drops specific code snippets, error messages, and the details inside intermediate reasoning. If a session first handles task A (say, changing an ETL entity), then a completely unrelated task B (say, adjusting a BI card layout), task A's context gets compressed into a summary to make room for task B. If task A later needs revisiting, the AI gets the summary, not the original context, and loses the key details (the real field value found during some debugging session, the exact wording of some SQL).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieval precision is diluted by irrelevant history.&lt;/strong&gt; Even without compression, mixing multiple tasks in one session means that when the AI works on the current task, the context window is full of dialogue from unrelated tasks (earlier task's tool calls, file reads, discussions). This content is not auto-filtered. It mixes with the current task's signal and dilutes the usable information density the model can draw on when generating its reply — showing up as unstable relevance judgments and a tendency to quote context details from the wrong task.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blurred task boundaries cause scope misjudgment.&lt;/strong&gt; When multiple tasks crowd one session, "what is the blast radius of this change" becomes hard to answer. Both AI and user confuse "changes required by the current task" with "changes left over from earlier tasks," raising the risk of over-broad operations (unrelated changes mixed into a &lt;code&gt;git diff&lt;/code&gt;) or missed consumer audits.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A concrete reference point: the "runaway $60, 7-hour session" in the circuit-breaker section was on the surface scope creep, but it also contained multi-task accumulation — one session finished the original task, then picked up a new request, then spawned more sub-tasks from it, never once asking "is this the point to open a new session," until the whole session's context kept diluting and the cost kept climbing linearly, while no single task was actually complex.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The difference from the circuit breaker:&lt;/strong&gt; the circuit breaker targets boundary creep &lt;em&gt;within&lt;/em&gt; a single task (one thing growing bigger and bigger, its impact spreading from one directory to several, a new tech stack pulled in). One-session-one-task targets multiple &lt;em&gt;independent&lt;/em&gt; tasks being forced into one session (even when each task is cleanly scoped and not creeping). The recognition signals differ, but the shared essence is: context signal density is a finite resource, and any form of "accumulation" (accumulating scope, accumulating tasks) dilutes that resource and drags down the AI's performance for the rest of the session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Push/deploy requires human approval
&lt;/h3&gt;

&lt;p&gt;Any &lt;code&gt;git push&lt;/code&gt;, deployment, &lt;code&gt;gh pr create&lt;/code&gt;, or other production-affecting operation must stop and wait for the user to say an explicit confirmation word ("push", "deploy", etc.) before executing. Asking "push?" and then immediately executing it yourself is not waiting — that is the common violation pattern. Before asking, you must have already finished the blast-radius assessment and local build/lint.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Change Management: Blast-Radius Assessment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The mandatory grep checklist after a field/function/definition change
&lt;/h3&gt;

&lt;p&gt;After renaming a field, changing a function signature, changing provider behavior, or changing a unit's precision or an enum value, you must: grep the whole project for that field/function name, classify every hit as a producer (writes the value) or a consumer (reads/computes/validates/displays it), and verify each consumer aligns. Changing one point and assuming the rest is fine is the easiest way to leave a trap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grep-first for bug fixes
&lt;/h3&gt;

&lt;p&gt;When fixing a bug, first grep the whole project for other occurrences of the same pattern, make a list, then fix them all in one pass — not just the one instance in the report. This matters especially for AI-generated code: an AI's bad habits are systematic, not isolated. A bad pattern it introduced in one place is likely present everywhere it was asked to do something similar. Before accepting any AI-suggested fix, grep for the pattern first — not after.&lt;/p&gt;

&lt;h3&gt;
  
  
  The completion-checklist pattern
&lt;/h3&gt;

&lt;p&gt;For a Metabase card/warehouse change, the full completion checklist is: grep out every consumer (card SQL, scripts, docs) → align each one → sync the IaC script → sync the doc index → update the related guide → verify with &lt;code&gt;execute_card&lt;/code&gt; → pass compile/syntax check → confirm the field quick-reference description matches the real data. All eight steps; skipping one "because it's a small change" is not allowed.&lt;/p&gt;

&lt;h3&gt;
  
  
  "An acceptable difference" is a proposition to prove, not a default premise
&lt;/h3&gt;

&lt;p&gt;In a recent budget-execution reconciliation session, the AI's first round concluded "budget 100% matched, execution rate differs by 1.35%/0.15%, acceptable precision," and prepared to wrap up. The user merely quoted that sentence back and asked for a detailed explanation. The AI did not treat this as a question to brush off. It drilled down again: first decomposing the difference with material-level data, then using the field tool to discover that the key field's caption meant something completely different from the assumption ("purchase-requisition tax-inclusive amount," not "wafer purchase amount"), and layering in the report's time-window boundary (orders placed the same day not yet included) — together fully explaining the 1.35%.&lt;/p&gt;

&lt;p&gt;This case proves two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Field names and captions being taken at face value is a systematic risk in complex ERP scenarios; you cannot assume a field's literal name equals its actual semantics.&lt;/li&gt;
&lt;li&gt;"The difference cannot be further explained" and "the difference has not yet been further investigated" are completely different things. The AI should strictly separate them, and must not deliver the latter dressed up as the former — even if the user doesn't press for an explanation right now.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  6. Document System Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lazy-loaded documents
&lt;/h3&gt;

&lt;p&gt;Not every document should be read every session. Each document should be labeled with its "central purpose + when to read it" trigger condition, loaded on demand rather than all-at-once by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is not just about saving tokens; it is about several concrete limits of the LLM context mechanism:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The context window is a finite and expensive shared resource; documents loaded by default crowd out space meant for the actual task.&lt;/strong&gt; System prompts, dialogue history, tool-call results, and code snippets all compete for the same window. If you stuff a dozen low-frequency documents (deployment manual, historical architecture decisions, some edge module's field notes) into every session's system prompt, you spend a big chunk of budget before the task even starts, leaving less room for the code and context you actually need to process, and making compression (replacing early content with a lossy summary) more likely to trigger.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long context dilutes the model's attention distribution; it does not uniformly "remember more."&lt;/strong&gt; The Transformer's self-attention can theoretically attend to any position, but in practice the model exploits long context unevenly — training and inference both show "content near the start and the end is easier to use, the middle gets ignored or down-weighted" (the so-called "lost in the middle"). If you cram a dozen irrelevant documents in, the few key rules that actually need to be cited may land exactly in that diluted middle, and the model is more likely to miss them when generating its reply.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Irrelevant document content creates retrieval noise, lowering the signal weight of key information.&lt;/strong&gt; Even if the model hasn't physically "forgotten" a passage, a mass of irrelevant text dilutes effective information density. The rule the current task needs, mixed in with a dozen unrelated documents, forces the model to judge relevance over a much larger candidate pool — and the more noise, the more the judgment drifts (quoting a near-identical rule from the wrong document, or conflating two documents' contents). The intuition matches human reading: finding one sentence on a single page is easy; finding the same sentence across a hundred pages of mixed documents raises the error rate sharply, even if "every word was technically seen."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lazy loading turns "the document" from static context into a controllable retrieval action, so trigger conditions can do precise matching instead of relying on the model to pick the relevant passage out of a sea of text.&lt;/strong&gt; Labeling each document with "central purpose + when to read it" is fundamentally moving the "information retrieval" task from inside the model (implicit relevance judgment in an over-long context) to outside it (deterministic loading by explicit trigger). Deterministic matching is inherently more reliable than having the model "notice" the right passage inside a diluted long context.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why the project's document system explicitly requires each document to be labeled "lazy-load: don't read every session; load on demand by trigger condition." That is not documentation-management fastidiousness; it is a direct response to the mechanism above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Document-code sync rules
&lt;/h3&gt;

&lt;p&gt;Which cache/explainer to update for each authoritative change should be an explicit mapping table (add a field → entity config + quick-reference table; change warehouse logic → new migration + warehouse-table quick-reference + related guide; change a card → IaC script + card index + guide). Without this mapping table, documentation drift is an inevitability, not a probability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirement-document tracking
&lt;/h3&gt;

&lt;p&gt;When requirements change, check against the requirement-tracking document first, and remind the user to update the requirement document before changing code; after the code changes, update the requirement document. This prevents the decoupled state where "the requirement was said out loud, the code is already running in production, but no written record of the scope change exists."&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Review and Verification Habits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Verify locally before pushing
&lt;/h3&gt;

&lt;p&gt;Before asking "push?", you must have run the local build/lint and finished the producer/consumer audit. A CI failure that is a build error you could have caught locally is pure wasted time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The verification checklist must explicitly separate "verified" from "not verified"
&lt;/h3&gt;

&lt;p&gt;A delivery summary must not vaguely say "should be fine." It must list exactly what was verified (&lt;code&gt;go build&lt;/code&gt; passed, YAML parses, SQL checked, end-to-end numbers reconciled) and what was not yet verified. For safety-sensitive changes (auth, production infrastructure, data-write paths), this distinction matters especially.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-card / cross-metric identity verification
&lt;/h3&gt;

&lt;p&gt;A recent dashboard-definition cross-validation session showed a method worth generalizing: first sort the relationships between cards into categories (hard identities, aggregate vs detail, tab-1 aggregate vs tab-2 detail, close-but-not-directly-comparable definitions), manage the verification list with a todo list, then batch-run and compare numbers — instead of hand-picking individual cards to check.&lt;/p&gt;

&lt;p&gt;The value is not just confirming the already-suspected points — during the systematic verification it found, as a byproduct, two real definition bugs nobody had noticed before (one card's total column mixed in a category that should have been excluded, another card missed a filter). "First list the reconciliation relationships, then run each one" covers far more than "spot-check the known suspicions," and it can surface unexpected problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth making into a permanent asset:&lt;/strong&gt; this kind of identity list deserves to be its own document section (for example, a fixed "card reconciliation-relationship list" section in the dashboard's technical-definition doc). Then after changing a warehouse definition, you can run the regression check directly from the list instead of re-deriving from scratch which cards have identity relationships with each other — the list itself is a reusable set of regression test cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automating the investigation flow: the data-sync diagnostic agent
&lt;/h3&gt;

&lt;p&gt;The data refresh pains — migration error states, ODS sync failures, warehouse tables not refreshing in time — are fundamentally the same kind of repetitive work: each time, a human (or the AI, ad-hoc) has to re-walk the flow of "check which table's timestamp, judge whether it's actually broken, find the fix command, execute, confirm." The project sedimented this flow into an independently running diagnostic agent. That is the concrete implementation of the principle "structure and normalize recurring investigation work instead of reinventing it each time."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem it solves:&lt;/strong&gt; it checks the health of the data-sync link on a daily schedule (ODS-layer sync success/failure/staleness, whether each warehouse fact table's refresh time exceeds the threshold), analyzes root causes, generates a fix plan, shows it to a human through a web UI for approval, and only actually executes the fix after approval. The core design is a three-stage pipeline: diagnose (fully automatic) → approve (a human clicks confirm in the web UI) → fix (auto-executes after approval, streaming logs in real time).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix-permission boundary, exactly consistent with the "red line" chapter's logic:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What can auto-execute without approval is only "re-run a read-only warehouse refresh function" — essentially a recomputation that changes no code or config.&lt;/li&gt;
&lt;li&gt;Anything that changes code or config (editing YAML, adding a migration) requires human approval before executing, and that approval triggers the normal git push + CI deployment flow, not bypassing version control to change production directly.&lt;/li&gt;
&lt;li&gt;For problems outside the system (like the known SQL Server account-permission gap), the agent does not try to auto-fix; it only shows a notification, because the authority to solve that class of problem is outside the code and needs an admin to intervene.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This three-level permission split is an extension of the same design philosophy as "the red line must enumerate tool names, not summarize a class in one sentence" from section 2. It is not a vague "auto-diagnose, auto-fix"; it explicitly splits "diagnose" and "execute" into two stages with different risk levels — read-only recomputation can run automatically, anything touching code must stop at the approval step, and where that boundary is drawn was thought through in advance, not left to on-the-spot judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The self-evolution mechanism echoes the "memory system" design:&lt;/strong&gt; after a human clicks "ignore" on an alert, that decision is written into an experience file, and the next diagnosis references it and stops re-reporting the same alert; every fix-execution success or failure is also recorded into a history log for later analysis to reference. This is the same idea as the project memory the AI uses during collaboration — let the system accumulate, through repeated runs, a model of "what is normal here, what is abnormal, what fix works," instead of judging from zero every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Its relationship to the "verification habits" section:&lt;/strong&gt; what the diagnostic agent does is essentially running the rule "the verification checklist must explicitly separate verified from unverified" as a daily automatic standing process. The metrics it checks (ODS last-successful-sync time, whether each warehouse table's refresh timestamp exceeds the staleness threshold) are exactly the things the "did the refresh actually take effect" discussion above wanted to confirm — except here the AI no longer has to check them ad-hoc after every code change; an independently running system keeps checking them in the background, and a human only steps in when it reports a problem. This also shows a natural direction of extension for the methodology: recurring high-frequency investigation work, past a certain stage, deserves to be upgraded from "human hand-execution each time" to "sedimented into an independently running standing system."&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Memory and Context Management
&lt;/h2&gt;

&lt;h3&gt;
  
  
  When to write project memory: the system does it automatically, without a user reminder
&lt;/h3&gt;

&lt;p&gt;Distinguish three signal types: the user explicitly corrects or confirms a preference (high priority, store immediately); the AI discovers a systematic trap or domain fact on its own (no need to wait for a user prompt, store after verifying); a one-off instruction local to the task (do not store). The judgment criterion is "would a future session waste time rediscovering this."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Judging the trigger timing and the content are both done by the AI autonomously in the current session, without human triggering.&lt;/strong&gt; If the memory system only wrote when the user explicitly asked, its coverage would be very limited. A large share of genuinely valuable lessons — a field whose real meaning differs from its surface name, a tool parameter that behaves specially in some scenario — are discovered by the AI during an investigation while the user isn't even present to witness the discovery, so there is no chance to remind it to "remember this." The mechanism therefore has to be the system default: after every round of investigation or change, the AI judges on its own whether this session produced knowledge worth reusing across sessions, and writes it immediately if so, without waiting for the user to speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And this accumulation self-evolves as the project develops; it is not built once and frozen.&lt;/strong&gt; Early on the memory store is empty. With each real stumble (permission boundary, field semantics, data definition, tool usage), a new entry is added. Later sessions, hitting a similar scenario, first check existing memory to see whether this trap has already been stepped in — if so, reuse the conclusion directly instead of re-exploring. As the project advances, this memory store's coverage and accuracy keep thickening — that is what "self-evolution" means. It is not one concentrated cleanup that pours all knowledge in at once; it is the daily, automatic sedimenting of newly discovered lessons, and the more it accumulates, the denser the knowledge becomes, and the AI's judgment on this project keeps improving with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory must be fine-grained, and the title must represent the whole content
&lt;/h3&gt;

&lt;p&gt;One memory entry covers exactly one independent behavioral rule or fact. If the content contains two independent behaviors, split it into two entries. Otherwise the future LLM will only load this memory when it thinks of the topic in the title, and the other half of the content gets silently ignored.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. A Catalog of Anti-Patterns
&lt;/h2&gt;

&lt;p&gt;The cases below all use the same structure: symptom → root cause → fix → the rule it sedimented into.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 1: The production-database "safety test" misjudgment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; to verify an account's permission boundary, directly execute a write operation (wrapped in ROLLBACK).&lt;br&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; equating "the operation ultimately caused no data damage" with "the operation is safe," ignoring that the write intent and lock contention are themselves the risk.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; use read-only catalog queries to verify the permission boundary instead.&lt;br&gt;
&lt;strong&gt;Rule sedimented:&lt;/strong&gt; the red-line section at the top of the rules file, explicitly forbidding any form of write, with no exemption for "looks harmless" workarounds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 2: "An acceptable difference" concluded too early
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; a reconciliation showed a 1.35% difference, and the first-round conclusion was "acceptable precision."&lt;br&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; packaging "can't explain it yet" as "can't explain it further," without drilling down to the field-semantics layer.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; after the user pressed, drilling down again revealed two eliminable root causes — a field-caption misread plus the report's time-window boundary.&lt;br&gt;
&lt;strong&gt;Rule sedimented:&lt;/strong&gt; a difference conclusion must explicitly separate "investigated to the bottom and confirmed as precision loss" from "not yet investigated and shelved as a known limitation"; before delivering, default to investigating further rather than shelving.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 3: &lt;code&gt;_is_deleted&lt;/code&gt; soft-delete flag not reset on upsert
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; a full-snapshot table's upsert only updated the business columns and forgot to reset &lt;code&gt;_is_deleted&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;, so records that had been soft-deleted and then reappeared were permanently filtered out downstream by &lt;code&gt;WHERE NOT _is_deleted&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; writing the &lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt; clause while only thinking of business fields, not realizing the soft-delete flag is also a state field that must be re-evaluated on every upsert.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; the upsert's update clause explicitly appends &lt;code&gt;_is_deleted = false&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;Rule sedimented:&lt;/strong&gt; any upsert logic touching a soft-delete flag must explicitly check "when this record reappears, should the soft-delete state be reset."&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 4: Dashcard-level visualization settings override card-level settings
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; a pivot table renders correctly on the card's own page, but degrades to the default display once placed on a dashboard (a stacked bar chart becomes a single line chart).&lt;br&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; when rendering a card inside a dashboard, the BI tool uses the dashcard's &lt;code&gt;visualization_settings&lt;/code&gt;, not the card's own; only the card-level settings were changed, and the dashcard-level ones were never synced.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; pivot/stacking settings must be written to both the card and the dashcard.&lt;br&gt;
&lt;strong&gt;Rule sedimented:&lt;/strong&gt; when debugging "why don't my card settings show up on the dashboard," the first step is always to check the dashcard's &lt;code&gt;visualization_settings&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Afterword: The Common Underlying Logic
&lt;/h2&gt;

&lt;p&gt;Compress the nine sections above into one sentence: &lt;strong&gt;the AI handles "how", and the human decides "whether to do it, how far, and by whose standard."&lt;/strong&gt; Connecting the two is a set of rules that are explicit, auditable, and exhaustive — not dependent on how careful the AI happens to be right now, and not dependent on the human remembering to remind it every time. The red line covers the tool-call surface by enumeration; the change audit covers consumers by grep; the verification checklist covers the boundary between "done" and "not done"; the document map covers "what change maps to which doc." The value of the methodology is not how comprehensive it reads. It is that every rule has a real, stepped-on trap as its reason for existing.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>softwareengineering</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Compaction Is a Control Problem: Static Boundaries, Dynamic Cut Points, and the Limits of Compression</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Sun, 20 Sep 2026 01:33:48 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/compaction-is-a-control-problem-static-boundaries-dynamic-cut-points-and-the-limits-of-j4m</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/compaction-is-a-control-problem-static-boundaries-dynamic-cut-points-and-the-limits-of-j4m</guid>
      <description>&lt;p&gt;An agent has just received a large test log. The current context window is near its budget. The final line may be the evidence needed to finish diagnosis; the model may need one more tool call to turn that evidence into a safe repair. But continuing blindly could leave no room for the next model invocation at all.&lt;/p&gt;

&lt;p&gt;That is the real compaction problem. It is not choosing a clever percentage. It is deciding who has authority to protect a finite resource, who has enough task knowledge to choose a good moment to switch, and what must survive the switch.&lt;/p&gt;

&lt;p&gt;This article is a design reading of the Codex implementation, not a file-by-file tour. The source tells us &lt;em&gt;how&lt;/em&gt; the system works; the interesting question is &lt;em&gt;why the responsibilities are divided this way&lt;/em&gt;, and which failures that division can or cannot handle.&lt;/p&gt;

&lt;p&gt;The analysis is pinned to &lt;a href="https://github.com/openai/codex/commit/8444cf63b50a8a88521e0d2970d49f659b48eac7" rel="noopener noreferrer"&gt;OpenAI Codex commit &lt;code&gt;8444cf63b50a8a88521e0d2970d49f659b48eac7&lt;/code&gt;&lt;/a&gt;. It describes that source tree, not an immutable product contract. It follows &lt;a href="https://jczhu.com/blog/codex-memory-internals/" rel="noopener noreferrer"&gt;Codex Memory Internals&lt;/a&gt;, which separates short-term compaction from durable memory, session storage, and project instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Design Problem: Continue Safely Without Cutting Blindly
&lt;/h2&gt;

&lt;p&gt;A long-running agent has two kinds of state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a durable record of what happened—messages, tool calls, files, and results;&lt;/li&gt;
&lt;li&gt;a much smaller &lt;strong&gt;active working window&lt;/strong&gt; that the next model invocation can actually use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;strong&gt;rollover&lt;/strong&gt; ends one active window and begins another. In Codex, &lt;em&gt;compaction&lt;/em&gt; is the lifecycle around that transition. It may replace old history with a local or remote compacted representation, or it may start a fresh managed window without asking a model to summarize anything. The design question is the same in every case: how should the system cross a context boundary without losing the task?&lt;/p&gt;

&lt;p&gt;Two naive answers both fail.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always switch at one fixed percentage.&lt;/strong&gt; This guarantees capacity, but the runtime cannot tell whether the agent has just reached the end of a useful investigative phase or is halfway through one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the model decide whenever to switch.&lt;/strong&gt; The model has the best view of task semantics, but it cannot safely be the final authority over capacity. It can be late, distracted, or wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Codex's answer is a hybrid control design: runtime owns the non-negotiable limits; the model receives a narrow, bounded chance to select a less destructive cut point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism One: Static Boundaries Give Runtime the Final Say
&lt;/h2&gt;

&lt;p&gt;The first mechanism is deliberately uncreative. Runtime tracks context pressure and moves the session before it becomes unsafe.&lt;/p&gt;

&lt;p&gt;The pinned source derives an automatic-compaction budget from 90% of the raw context window and sets a default effective full-context guard at 95%. The numbers themselves are policy; the design is the important part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;90%: begin the normal transition policy while there is still room.
95%: do not allow total active context to cross this runtime guard.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two thresholds separate &lt;strong&gt;planned rollover&lt;/strong&gt; from &lt;strong&gt;last-resort prevention&lt;/strong&gt;. In the ordinary &lt;code&gt;Total&lt;/code&gt; path, without the optional fallback discussed below, 90% normally triggers first and the session never reaches 95%. That does not make 95% pointless. It keeps the total-context safety contract independent from the policy that happens to schedule rollover today.&lt;/p&gt;

&lt;p&gt;This is a runtime policy, not a second provider-side measurement. The transferable principle is simple: &lt;strong&gt;the component that can enforce a resource limit must retain the final veto&lt;/strong&gt;. A model can propose timing. It should not be trusted to guarantee that there will still be room for the next request.&lt;/p&gt;

&lt;p&gt;OpenAI's public API guidance describes the same operational posture at a higher level: monitor usage, plan ahead, compact after major milestones rather than every turn, and preserve functionally equivalent instructions when resuming. &lt;a href="https://developers.openai.com/api/docs/guides/latest-model?model=gpt-5.2#compaction" rel="noopener noreferrer"&gt;OpenAI Docs&lt;/a&gt; explains the API form; Codex implements a client-side policy around that general idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mechanism Two: A Bounded Dynamic Cut Point Uses Model Judgment Safely
&lt;/h2&gt;

&lt;p&gt;Static thresholds answer “must we stop extending this window?” They do not answer “is this a good moment to stop?”&lt;/p&gt;

&lt;p&gt;The optional TokenBudget fallback creates a small, controlled interval after the normal automatic budget is exhausted. Runtime appends one fallback prompt to the model-visible conversation and lets the model take one more bounded step. The model can then call &lt;code&gt;new_context&lt;/code&gt; if the task has reached a semantic cut point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;automatic budget reached
    -&amp;gt; runtime opens a small bounded interval
    -&amp;gt; model sees the latest work and may request new_context
    -&amp;gt; runtime performs the rollover

no model request
    -&amp;gt; runtime forces rollover when the buffer ends or the total guard is reached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not delegation of context management. The model can request a transition; it cannot mutate the window, advance the history, or waive the limit. Runtime records the request and performs the state transition. The relevant implementation is split across the TokenBudget path, the &lt;code&gt;new_context&lt;/code&gt; tool handler, and the session state; the important design fact is the separation of &lt;strong&gt;semantic judgment&lt;/strong&gt; from &lt;strong&gt;capacity authority&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why is that useful? Runtime sees token counts but not task meaning. The model may know that the latest tool result completes diagnosis, so a new window can begin a repair phase cleanly. Or it may know that one more safe tool call is required before a transition would be coherent. The fallback buffer buys that judgment one bounded opportunity. If the model makes no request, runtime's static boundary remains the safety net.&lt;/p&gt;

&lt;p&gt;The buffer is therefore not extra model capacity, and it is not a second dynamic budget. Its size is static configuration. What is dynamic is the model's choice of &lt;em&gt;when inside that interval&lt;/em&gt; to ask for the rollover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Accounting Is a Design Boundary, Not a Single Counter
&lt;/h2&gt;

&lt;p&gt;The most common context bug is treating every token as if it had the same remedy. A useful design model keeps the sources of pressure separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;W  raw model context window
P  fixed prefix required by the next invocation
H  replayable session history
I  incoming material not yet recorded
   (new user input, context diff, or reinjection)
O  output and reasoning headroom
C  compaction-request payload, if compaction needs a summary

next invocation = P + H + I + O
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value of this model is diagnostic. If &lt;code&gt;H&lt;/code&gt; is large, changing its representation can help. If &lt;code&gt;P&lt;/code&gt; is large, compaction cannot help: system instructions, tool schemas, MCP definitions, and project rules must still be sent. If &lt;code&gt;I&lt;/code&gt; is the surprise, a count taken before the next user message or context injection is not enough. If &lt;code&gt;C&lt;/code&gt; is too large, the compactor itself needs a budget.&lt;/p&gt;

&lt;p&gt;This is why a single &lt;code&gt;used_tokens&lt;/code&gt; indicator is inadequate for an agent runtime. It cannot explain whether the next action should be compacting history, disabling a tool, choosing a larger-window model, rejecting an oversized injection, or investigating a provider error.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;BodyAfterPrefix&lt;/code&gt;: Do Not Punish Stable Setup, but Never Hide It
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BodyAfterPrefix&lt;/code&gt; is Codex's answer to a particular accounting problem: a large but stable setup can consume a sizeable share of every new window even when the session itself has barely progressed.&lt;/p&gt;

&lt;p&gt;The design separates two questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Has this window accumulated enough new work to justify a rollover?
Can the whole request—stable setup plus new work—still fit?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To answer the first, Codex remembers the input tokens from the first server-observed request in a window and charges later growth against the automatic budget. To answer the second, it continues to check total active context against the full-context guard. &lt;a href="https://github.com/openai/codex/blob/8444cf63b50a8a88521e0d2970d49f659b48eac7/codex-rs/core/src/state/auto_compact_window.rs" rel="noopener noreferrer"&gt;&lt;code&gt;auto_compact_window.rs&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/openai/codex/blob/8444cf63b50a8a88521e0d2970d49f659b48eac7/codex-rs/core/src/session/context_window.rs" rel="noopener noreferrer"&gt;&lt;code&gt;context_window.rs&lt;/code&gt;&lt;/a&gt; are the implementation evidence for this split.&lt;/p&gt;

&lt;p&gt;The policy is not “ignore the prefix.” It is “do not spend the session-growth budget repeatedly on fixed setup, while still refusing to exceed the total limit.” If the prefix alone nearly fills the usable window, that is not a compaction problem. It is a configuration-admission problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Compaction Stops Being the Answer
&lt;/h2&gt;

&lt;p&gt;The static and dynamic mechanisms manage a viable session whose replayable history is growing. Several nearby failures need a different design response.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure class&lt;/th&gt;
&lt;th&gt;Why rollover alone is insufficient&lt;/th&gt;
&lt;th&gt;Design response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed prefix already fills the window&lt;/td&gt;
&lt;td&gt;No old history exists to remove.&lt;/td&gt;
&lt;td&gt;Measure prefix feasibility before first sampling; reduce configuration or select a larger window.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pending input or reinjection crosses the limit&lt;/td&gt;
&lt;td&gt;The previous request fit, but the next complete invocation does not.&lt;/td&gt;
&lt;td&gt;Preflight the next request, including pending additions and output headroom.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A tool loop needs another model call&lt;/td&gt;
&lt;td&gt;A reset can detach the continuation from its current task.&lt;/td&gt;
&lt;td&gt;Preserve active work state and the user task across the transition.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model switch changes capacity or compatibility&lt;/td&gt;
&lt;td&gt;The next consumer is different even though history did not grow.&lt;/td&gt;
&lt;td&gt;Validate the target model and migrate the window deliberately.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction request overflows&lt;/td&gt;
&lt;td&gt;The repair operation exceeds its own input budget.&lt;/td&gt;
&lt;td&gt;Bound or chunk compaction input and retain an explicit exact tail.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider errors recur late in a session&lt;/td&gt;
&lt;td&gt;Length is only one possible cause.&lt;/td&gt;
&lt;td&gt;Classify the error and compare a smaller retry before treating it as compaction pressure.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The source makes two of these limits unusually visible. A TODO beside pre-turn compaction says the runtime should estimate pending context updates and the new user message before deciding whether to roll over. That is a design admission: observing the old window is not the same as admitting the next invocation. And local compaction removes oldest history items and retries if its own request overflows, showing that a compactor needs a bounded-input policy of its own. See &lt;a href="https://github.com/openai/codex/blob/8444cf63b50a8a88521e0d2970d49f659b48eac7/codex-rs/core/src/session/turn.rs" rel="noopener noreferrer"&gt;&lt;code&gt;turn.rs&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/openai/codex/blob/8444cf63b50a8a88521e0d2970d49f659b48eac7/codex-rs/core/src/compact.rs" rel="noopener noreferrer"&gt;&lt;code&gt;compact.rs&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Transition Contract, Several Implementations
&lt;/h2&gt;

&lt;p&gt;The trigger policy answers &lt;strong&gt;when&lt;/strong&gt; to leave a window. A separate strategy layer answers &lt;strong&gt;how&lt;/strong&gt; to make the next one usable.&lt;/p&gt;

&lt;p&gt;Codex can select a remote compaction path, a local replacement-history path, or the TokenBudget path that starts a fresh managed window without model or server summarization. These differ in information preservation and provider dependency, but they should uphold one contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;after the transition, the next model invocation has a valid context
and enough task state to continue correctly.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why “compaction” should not be treated as a summary algorithm. It is a state transition with pluggable mechanisms. The dispatch in &lt;a href="https://github.com/openai/codex/blob/8444cf63b50a8a88521e0d2970d49f659b48eac7/codex-rs/core/src/session/turn.rs" rel="noopener noreferrer"&gt;&lt;code&gt;run_auto_compact&lt;/code&gt;&lt;/a&gt; and the fresh-window TokenBudget path in &lt;a href="https://github.com/openai/codex/blob/8444cf63b50a8a88521e0d2970d49f659b48eac7/codex-rs/core/src/compact_token_budget.rs" rel="noopener noreferrer"&gt;&lt;code&gt;compact_token_budget.rs&lt;/code&gt;&lt;/a&gt; make that architecture explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the Continuity Contract Before Choosing the Algorithm
&lt;/h2&gt;

&lt;p&gt;The quality of a transition is not its compression ratio. It is whether the agent can still do the same work without rediscovering or contradicting itself.&lt;/p&gt;

&lt;p&gt;For a coding agent, the transition must preserve or make retrievable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the objective and non-negotiable user constraints;&lt;/li&gt;
&lt;li&gt;decisions already made and their rationale;&lt;/li&gt;
&lt;li&gt;verified facts from tools and tests;&lt;/li&gt;
&lt;li&gt;exact files, identifiers, commands, errors, and in-progress changes;&lt;/li&gt;
&lt;li&gt;unfinished work, blockers, and the next safe action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeated compaction degrades quality because a summary is not the original record. When a detail is lost and no durable artifact or retrieval path can restore it, later summaries cannot recreate it. The remedy is not merely a longer summary. It is to keep durable files, results, and references outside the hot context window, then make the transition state point back to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for OpenCode
&lt;/h2&gt;

&lt;p&gt;The OpenCode reports that prompted this investigation fit the same design map. &lt;a href="https://github.com/anomalyco/opencode/issues/48844" rel="noopener noreferrer"&gt;Issue #48844&lt;/a&gt; describes a compaction request that is itself too large. &lt;a href="https://github.com/anomalyco/opencode/issues/48847" rel="noopener noreferrer"&gt;#48847&lt;/a&gt; describes fixed system, tool, and project overhead exhausting the usable window. &lt;a href="https://github.com/anomalyco/opencode/issues/48370" rel="noopener noreferrer"&gt;#48370&lt;/a&gt; describes provider errors that recur as context grows. These are issue reports, not proof of one cause, but they should not all receive “compact earlier” as the answer.&lt;/p&gt;

&lt;p&gt;An OpenCode context-health surface should expose design decisions, not just token totals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capacity safety:&lt;/strong&gt; fixed prefix, replayable history, pending additions, output reserve, automatic budget, and full-context guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transition timing:&lt;/strong&gt; whether a bounded dynamic cut point is available, whether the model requested rollover, and how much room remains before runtime must take over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure classification:&lt;/strong&gt; prefix infeasibility, compactor size, model-switch compatibility, and provider errors kept separate from normal history growth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turns a context-limit error from an opaque event into an operational decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Principle
&lt;/h2&gt;

&lt;p&gt;Compaction is not “summarize when the percentage is high.” It is a control problem with a clear division of labor.&lt;/p&gt;

&lt;p&gt;Use deterministic runtime boundaries to guarantee that the agent can continue safely. Within a small bounded interval, let the model use its task understanding to request a less disruptive transition. Keep the actual state change in runtime. And do not mislabel fixed-prefix failures, compactor overflows, model changes, or provider defects as problems a summary can solve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static boundaries protect capacity. Dynamic cut points protect continuity. A reliable agent needs both—and must know when neither is the right tool.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aicoding</category>
      <category>codex</category>
      <category>opencode</category>
      <category>agentworkflow</category>
    </item>
    <item>
      <title>OpenCode Needs Memory, Not Another Background Pipeline</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Thu, 27 Aug 2026 09:14:39 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/opencode-needs-memory-not-another-background-pipeline-17j9</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/opencode-needs-memory-not-another-background-pipeline-17j9</guid>
      <description>&lt;p&gt;I kept explaining the same project facts to new OpenCode sessions.&lt;/p&gt;

&lt;p&gt;Not the durable rules that already belonged in &lt;code&gt;AGENTS.md&lt;/code&gt;. Those were present. The repetition was smaller and more frustrating: a project-specific convention discovered while fixing a bug, a user preference expressed during a review, or a workflow detail that was useful but too narrow to turn into permanent team policy.&lt;/p&gt;

&lt;p&gt;OpenCode already preserves sessions, loads explicit instructions, and compacts long conversations. Those are valuable systems, but none answers a simple cross-session question: after an agent learns something reusable in this project, how does the next session know it?&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/chncaesar/opencode-plugin-memory" rel="noopener noreferrer"&gt;opencode-plugin-memory&lt;/a&gt; as a deliberately small answer. It gives the active OpenCode agent four memory tools, stores knowledge as project-local Markdown, and automatically injects a compact index into future system prompts.&lt;/p&gt;

&lt;p&gt;The point is not to make every conversation permanent. The point is to stop rediscovering the lessons that should survive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between instructions and history
&lt;/h2&gt;

&lt;p&gt;There are two tempting but incomplete answers to persistent agent knowledge.&lt;/p&gt;

&lt;p&gt;The first is &lt;code&gt;AGENTS.md&lt;/code&gt;. It is the right place for rules that must govern behavior: required verification commands, architectural boundaries, security constraints, and repository conventions. It is explicit, reviewable, and authoritative. A model should not have to infer a release requirement from a previous chat.&lt;/p&gt;

&lt;p&gt;The second is session history. It records what happened, but a new session does not automatically search old conversations for a relevant decision. Even if it did, raw history is a poor instruction source. It contains tentative ideas, failed attempts, stale facts, and untrusted tool output alongside useful conclusions.&lt;/p&gt;

&lt;p&gt;The missing layer is selective recall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AGENTS.md       -&amp;gt; mandatory, authored instructions
session history -&amp;gt; evidence about one conversation
project memory  -&amp;gt; selected lessons useful in later sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction came out of my earlier source-level analyses of &lt;a href="https://jczhu.com/blog/opencode-memory-internals/" rel="noopener noreferrer"&gt;what OpenCode actually remembers&lt;/a&gt; and &lt;a href="https://jczhu.com/blog/codex-memory-internals/" rel="noopener noreferrer"&gt;how Codex runs its local-memory pipeline&lt;/a&gt;. OpenCode core intentionally stops short of a general long-term memory service. Codex takes the opposite route: a background, two-model pipeline extracts and consolidates eligible prior rollouts.&lt;/p&gt;

&lt;p&gt;Both choices make sense. But for a single developer working inside one repository, I wanted a third option: no cloud service, no vector database, no delayed consolidation worker, and no opaque state outside the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  A memory layer the active agent owns
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;opencode-plugin-memory&lt;/code&gt; follows a hot-path design. The active agent decides whether a lesson is worth retaining while it has the task context that makes the judgment meaningful.&lt;/p&gt;

&lt;p&gt;It registers four tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;memory_add&lt;/code&gt; saves a reusable project fact or convention.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory_update&lt;/code&gt; corrects or expands an existing memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory_delete&lt;/code&gt; archives an obsolete entry instead of erasing it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory_read&lt;/code&gt; retrieves full entries, optionally filtered by keyword.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool descriptions give the agent a narrow policy. It should save an explicit "remember this" request, a recurring bug lesson, a project convention, or a correction that future sessions should respect. It should not save the file it happens to be editing, generic programming advice, or information already documented in &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That policy matters more than a large store. A memory system that records every task turns future prompts into an untrusted, noisy transcript. This plugin asks for small, deliberate promotion decisions at the moment the evidence is fresh.&lt;/p&gt;

&lt;p&gt;The store is ordinary Markdown under the project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.opencode/memory/
  MEMORY.md
  memory_summary.md
  plugin.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MEMORY.md&lt;/code&gt; is the source of truth. You can open it in an editor, review what the agent has learned, correct an entry directly, or restore an archived item. The generated &lt;code&gt;memory_summary.md&lt;/code&gt; is only an index. It lists active memory IDs, titles, and tags within a configurable character budget.&lt;/p&gt;

&lt;p&gt;On every OpenCode system-prompt transform, the plugin appends that summary to the prompt. The agent has ambient awareness that the project has relevant memory without paying to load the entire handbook on every turn. When a title looks relevant, it calls &lt;code&gt;memory_read&lt;/code&gt; for the details.&lt;/p&gt;

&lt;p&gt;That is progressive disclosure with plain files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bounded summary in every prompt
        -&amp;gt; agent recognizes a relevant topic
        -&amp;gt; memory_read loads the full entry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is simpler than semantic retrieval and, for a compact project knowledge base, easier to inspect when the agent makes a bad decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Markdown is a feature
&lt;/h2&gt;

&lt;p&gt;Agent memory is often described as a retrieval problem. That framing tends to lead straight to embeddings, vector stores, hidden profiles, and ranking systems. Those tools can be appropriate when an application must search a large, multi-user corpus.&lt;/p&gt;

&lt;p&gt;They are not automatically appropriate for a coding project.&lt;/p&gt;

&lt;p&gt;For project memory, the harder questions are authority and lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this a hard rule, a reusable lesson, or a temporary hypothesis?&lt;/li&gt;
&lt;li&gt;Can a developer see exactly what will influence a future agent?&lt;/li&gt;
&lt;li&gt;What happens when an API, preference, or workflow becomes obsolete?&lt;/li&gt;
&lt;li&gt;Can a mistaken memory be corrected without administering another service?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markdown gives direct answers. The user owns the file. The agent's operations are visible. IDs are sequential, so references remain stable. Deletes are soft deletes: the entry moves to an &lt;code&gt;Archived&lt;/code&gt; section, preserving a review path rather than pretending old knowledge never existed.&lt;/p&gt;

&lt;p&gt;The plugin also maintains an optional append-only &lt;code&gt;plugin.log&lt;/code&gt; for add, update, delete, and error events. This is not a cryptographic audit trail, and it is not meant to be one. It is a practical way to see when the memory layer changed.&lt;/p&gt;

&lt;p&gt;Transparency is not only a usability choice. A memory entry becomes part of an agent's future decision context. A local, human-readable store makes that influence inspectable before it becomes mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it in one configuration block
&lt;/h2&gt;

&lt;p&gt;The plugin is available at &lt;a href="https://github.com/chncaesar/opencode-plugin-memory" rel="noopener noreferrer"&gt;github.com/chncaesar/opencode-plugin-memory&lt;/a&gt;. Add a local clone or package reference to OpenCode's plugin configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// opencode.json or ~/.config/opencode/opencode.jsonc&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/opencode-plugin-memory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxSummaryChars"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default summary budget is 2,000 characters, roughly 500 tokens. Raise it when a project has more distinct memory topics; keep it small when prompt cost matters more than ambient recall. Set &lt;code&gt;enableLog&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; if the operation log is not useful for your workflow.&lt;/p&gt;

&lt;p&gt;The memory directory is created on the first write. There is no server to provision, account to create, or migration to run.&lt;/p&gt;

&lt;p&gt;After installation, the useful interaction is intentionally ordinary. Tell the agent to remember a durable lesson when it appears. For example: after finding that a repository requires a specific local verification sequence, the agent can save the reusable workflow detail. In a later session, the summary exposes the title, and the agent can read the full entry before proposing a change.&lt;/p&gt;

&lt;p&gt;The result is not an agent that claims perfect recall. It is an agent with a visible project notebook that it can maintain itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this plugin does not try to solve
&lt;/h2&gt;

&lt;p&gt;Small scope is part of the design.&lt;/p&gt;

&lt;p&gt;It does not replace &lt;code&gt;AGENTS.md&lt;/code&gt;. Mandatory project rules should remain in explicit instructions, where they are always loaded and reviewed as policy. Memory is for selected recall, not the only copy of a requirement that must be followed.&lt;/p&gt;

&lt;p&gt;It does not mine every past session in the background. Codex's extraction and consolidation pipeline addresses a different problem: automatically turning a large body of historical work into a curated memory workspace. This plugin puts the decision in the active agent's tool call instead, avoiding a second model, delayed processing, and another trust boundary.&lt;/p&gt;

&lt;p&gt;It does not use a vector database. Search is keyword-based and the summary index provides ambient routing. That is a conscious trade-off for modest, project-scoped collections where inspectability is more valuable than fuzzy recall.&lt;/p&gt;

&lt;p&gt;It does not add concurrent-write locking. The intended model is normal single-user project work; concurrent OpenCode sessions writing the same &lt;code&gt;MEMORY.md&lt;/code&gt; need stronger coordination and tests before that complexity is justified.&lt;/p&gt;

&lt;p&gt;Those limitations are not hidden caveats. They define the product. If you need a background policy, evidence retention, semantic search across thousands of conversations, or multi-user conflict resolution, use a system designed for that scale. If you want a local memory layer that remains understandable in a text editor, this is the narrower tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useful middle ground
&lt;/h2&gt;

&lt;p&gt;The choice is not between manual instructions and an elaborate autonomous memory platform.&lt;/p&gt;

&lt;p&gt;For many OpenCode projects, there is a useful middle ground: keep rules in &lt;code&gt;AGENTS.md&lt;/code&gt;; keep conversations in their session records; let the active agent promote only the reusable lessons into a small, visible, project-scoped memory file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/chncaesar/opencode-plugin-memory" rel="noopener noreferrer"&gt;opencode-plugin-memory&lt;/a&gt; implements that middle ground. It makes continuity across sessions a deliberate, inspectable capability instead of a hope that the next agent will rediscover the same context.&lt;/p&gt;

&lt;p&gt;For adjacent tools that keep an OpenCode workflow reliable, including readiness checks, database maintenance, session reflection, and multi-machine coordination, see the &lt;a href="https://jczhu.com/opencode-tools/" rel="noopener noreferrer"&gt;OpenCode Reliability Toolkit&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aicoding</category>
      <category>opencode</category>
      <category>agentworkflow</category>
      <category>contextengineering</category>
    </item>
    <item>
      <title>Codex Memory Internals: What It Remembers, Who Decides, and How It Compares to OpenCode</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:38:08 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/codex-memory-internals-what-it-remembers-who-decides-and-how-it-compares-to-opencode-5bmm</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/codex-memory-internals-what-it-remembers-who-decides-and-how-it-compares-to-opencode-5bmm</guid>
      <description>&lt;p&gt;I began this investigation with a specific question: can Codex autonomously add, modify, and delete its own memories? The product documentation already says that it has memory. What I wanted to know was who actually decides what survives.&lt;/p&gt;

&lt;p&gt;When an old chat contains a useful build command, does deterministic application code copy it into a database? Does the active coding model call a memory tool? Does another model summarize the chat later? When the command becomes obsolete, is the old fact overwritten, invalidated, aged out, or simply left where future agents may still find it?&lt;/p&gt;

&lt;p&gt;Those questions led to a more interesting result than a feature checklist. Codex has a genuine cross-session memory subsystem, but its behavior is split between model judgment and deterministic lifecycle code. Models decide what a rollout means and how durable guidance should be rewritten. Runtime code decides which rollouts are eligible, which evidence remains in the working set, when old records are deleted, and when the consolidation model is allowed to run.&lt;/p&gt;

&lt;p&gt;That makes the short answer precise:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With local memories enabled, Codex can autonomously add, modify, merge, and remove persistent memory without a user approving each write. User-requested corrections follow a separate append-only note path, while retention, thread deletion, and reset provide additional forms of forgetting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rest of this article explains why each word in that answer matters.&lt;/p&gt;

&lt;p&gt;This analysis is pinned to &lt;a href="https://github.com/openai/codex/commit/8444cf63b50a8a88521e0d2970d49f659b48eac7" rel="noopener noreferrer"&gt;OpenAI Codex commit &lt;code&gt;8444cf63b50a8a88521e0d2970d49f659b48eac7&lt;/code&gt;&lt;/a&gt;, checked on August 25, 2026. The feature is marked stable in that source tree but remains off by default, so this describes implemented behavior, not behavior every Codex user is currently receiving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Codex local memory is a background two-model pipeline.&lt;/strong&gt; One model extracts reusable material from each eligible rollout. A second model consolidates those outputs into a global file-based memory workspace.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The LLM owns semantic CRUD, but not lifecycle scheduling.&lt;/strong&gt; Prompts ask the models to decide what is useful, merge new evidence, rewrite stale guidance, preserve uncertainty, and remove claims whose evidence disappeared. Rust and SQLite code choose candidates, enforce leases, rank inputs, prune old rows, and synchronize files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Forgetting has several independent mechanisms, not one delete operation.&lt;/strong&gt; Codex can delete an old extraction after a later no-op, drop rollout summaries that leave the selected set, ask the consolidation agent to remove unsupported guidance, prune unused database rows, remove expired extension resources, forget memory attached to a deleted thread, or reset the entire memory store.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retrieval is progressive disclosure, not vector RAG.&lt;/strong&gt; A bounded &lt;code&gt;memory_summary.md&lt;/code&gt; is injected into developer instructions. The model then searches &lt;code&gt;MEMORY.md&lt;/code&gt; and opens one or two supporting summaries or skills when needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Usage feeds retention.&lt;/strong&gt; Memory citations contain rollout IDs. When Codex emits a citation, runtime code increments usage counts and timestamps for the corresponding Phase 1 records. Later consolidation prefers frequently and recently used evidence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User corrections are indirect and permission-dependent.&lt;/strong&gt; After a direct request, Codex can create an append-only ad-hoc note when dedicated memory tools are enabled or the active permission profile separately permits the write. The next consolidation pass interprets that note and updates the generated memory artifacts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt;, session persistence, compaction, and long-term memory remain different systems.&lt;/strong&gt; Team rules belong in explicit instructions. Rollouts record what happened. Compaction keeps one conversation within its context budget. The memory pipeline promotes selected lessons across conversations.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Pipeline I Found
&lt;/h2&gt;

&lt;p&gt;Before the pipeline, a definition. A &lt;strong&gt;rollout&lt;/strong&gt; is Codex's term for the complete recorded history of one session: user messages, model responses, tool calls and outputs, turn boundaries, and session metadata, all persisted as a single file under the session directory. It is the raw transcript of one conversation, and it is the input the memory pipeline mines for reusable lessons.&lt;/p&gt;

&lt;p&gt;The implementation lives across four layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eligible prior rollouts
        |
        v
Phase 1 extraction model
        |
        v
SQLite stage1_outputs
        |
        v
selected evidence + git workspace diff
        |
        v
Phase 2 consolidation agent
        |
        v
memory_summary.md -&amp;gt; MEMORY.md -&amp;gt; summaries / skills / source rollouts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trigger is more precise than "when a session starts." Memory work is dispatched only after a fresh turn that carries user input has successfully started and the primary environment is configured. The "worker" here is the background task Codex spawns to run that memory pipeline — an asynchronous routine, not a separate process or an independent agent. It returns without doing anything if the session is ephemeral, the &lt;code&gt;memories&lt;/code&gt; feature is disabled, the session is a sub-agent rather than a root agent, or the state database is missing.&lt;/p&gt;

&lt;p&gt;Once dispatched, the worker runs in the background. It first deletes old, unused extraction rows, then checks the remaining Codex rate-limit percentage; if quota is low, it skips the model work rather than spend tokens near a limit.&lt;/p&gt;

&lt;p&gt;The defaults reveal the intended operating model. The &lt;code&gt;MemoryTool&lt;/code&gt; feature is stable but off by default, and enabling it turns on both generating and using memories. Extraction only looks back at the previous ten days of rollouts and skips anything that has been idle for fewer than six hours. Consolidation keeps at most 256 raw inputs at a time, and any memory that goes thirty days without use reaches the retention boundary and becomes eligible for pruning.&lt;/p&gt;

&lt;p&gt;Those numbers are cost and growth controls, not judgments about meaning. They decide which experiences are worth processing and how much state to keep; the models still decide what the surviving content actually means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 1: A Model Decides What A Rollout Taught
&lt;/h2&gt;

&lt;p&gt;Phase 1 scans recent interactive root threads whose per-thread memory mode is enabled. It excludes the current thread and rollouts that are too fresh, too old, already current, being processed elsewhere, or outside the bounded startup claim.&lt;/p&gt;

&lt;p&gt;For every claimed rollout, Codex loads the recorded items, filters them to memory-relevant model input, and sends the result to a dedicated extraction model with a strict JSON schema. The sanitizer drops developer messages, marked &lt;code&gt;AGENTS.md&lt;/code&gt; and skill injections, and compaction records before the rollout reaches the extractor. The model must return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;raw_memory&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rollout_summary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;an optional &lt;code&gt;rollout_slug&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt is unusually explicit about epistemic discipline. User messages are the strongest source for preferences. Tool outputs and verification evidence are the strongest source for repository facts. Assistant proposals should not silently become durable truth. Secrets must be redacted, temporary metrics should be skipped, and a no-op is preferred when a future agent would not plausibly act better because of the memory.&lt;/p&gt;

&lt;p&gt;This is autonomous creation, but it is selective creation. A successful extraction upserts one &lt;code&gt;stage1_outputs&lt;/code&gt; row per thread. If the source rollout later changes, a newer extraction replaces the old row. If the new model pass returns an empty result, Codex deletes any previous extraction for that thread and schedules consolidation so the higher-level files can forget what no longer has support.&lt;/p&gt;

&lt;p&gt;That last case is easy to miss. "No useful memory" is not only an absence of creation. On a regenerated rollout, it can become a deletion signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2: A Model Rewrites The Global Memory
&lt;/h2&gt;

&lt;p&gt;Phase 2 is not a simple concatenation job. It first takes a global lease — a timed lock with an ownership token and periodic heartbeats — so that only one consolidation task can touch the shared memory files at a time, and a crashed worker eventually releases the lock by letting its lease expire. Only then does it select a bounded set of Phase 1 records and materialize them under &lt;code&gt;~/.codex/memories/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The selection algorithm prefers &lt;code&gt;usage_count&lt;/code&gt;, then recent &lt;code&gt;last_usage&lt;/code&gt;, then source recency. Never-used records use their source update time as the fallback. Records outside the unused-memory window are ineligible.&lt;/p&gt;

&lt;p&gt;Runtime code rebuilds &lt;code&gt;raw_memories.md&lt;/code&gt;, writes one Markdown file per selected rollout under &lt;code&gt;rollout_summaries/&lt;/code&gt;, and removes old summary files that are no longer selected. The memory directory is managed as a small Git baseline repository. Codex computes a bounded diff from the previous successful consolidation and writes it to &lt;code&gt;phase2_workspace_diff.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If nothing changed and the required output files are valid, Phase 2 exits without calling a model. If inputs were added, modified, or deleted, it starts an internal consolidation agent with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;approval policy set to &lt;code&gt;Never&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;collaboration, apps, plugins, MCP servers, and memory recursion disabled;&lt;/li&gt;
&lt;li&gt;memory-root-only write access and no network when the parent uses a Codex-managed permission profile;&lt;/li&gt;
&lt;li&gt;the parent's enforcement choice preserved when Codex sandboxing is explicitly disabled or delegated to an external sandbox;&lt;/li&gt;
&lt;li&gt;a medium-reasoning consolidation model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The consolidation prompt gives that agent semantic ownership of three outputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MEMORY.md&lt;/code&gt;, the retrieval-oriented handbook;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory_summary.md&lt;/code&gt;, the compact index injected into future prompts;&lt;/li&gt;
&lt;li&gt;optional reusable packages under &lt;code&gt;skills/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;MEMORY.md&lt;/code&gt; has a deliberate structure that makes the read path work. Its top-level unit is a &lt;code&gt;# Task Group&lt;/code&gt; block, one per task family, headed by a &lt;code&gt;scope:&lt;/code&gt; line and an &lt;code&gt;applies_to:&lt;/code&gt; line that preserves the working-directory boundary so similar tasks in different checkouts are not confused. Inside each block, &lt;code&gt;## Task &amp;lt;n&amp;gt;&lt;/code&gt; sections come first, each carrying &lt;code&gt;### rollout_summary_files&lt;/code&gt; (with &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;rollout_path&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt;, and &lt;code&gt;thread_id&lt;/code&gt;) and &lt;code&gt;### keywords&lt;/code&gt; for grep-style retrieval. After the task list come three consolidated sections: &lt;code&gt;## User preferences&lt;/code&gt;, &lt;code&gt;## Reusable knowledge&lt;/code&gt;, and &lt;code&gt;## Failures and how to do differently&lt;/code&gt;, each bullet traceable back to task references like &lt;code&gt;[Task 1]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This shape is why retrieval can be lexical instead of vector-based: the routing handles live in stable headings and keywords, and the durable guidance lives just below them. &lt;code&gt;memory_summary.md&lt;/code&gt; then sits above &lt;code&gt;MEMORY.md&lt;/code&gt; as a denser, token-budgeted index rather than a second handbook.&lt;/p&gt;

&lt;p&gt;The agent is instructed to incrementally merge new evidence, update contradictory guidance, preserve uncertainty when validation is unclear, and minimize churn when existing material is still correct. It must retain task-level provenance through rollout-summary paths, thread IDs, working directories, and update timestamps.&lt;/p&gt;

&lt;p&gt;Most importantly, the prompt defines forgetting. Deleted rollout summaries and extension resources form a stale-cleanup queue. The agent searches for memory supported by those inputs, removes only unsupported guidance, preserves facts that still have other evidence, and then cleans the corresponding entries from the summary index.&lt;/p&gt;

&lt;p&gt;The runtime verifies that &lt;code&gt;MEMORY.md&lt;/code&gt; exists and that &lt;code&gt;memory_summary.md&lt;/code&gt; starts with the expected schema marker. It removes symlinks from the workspace. Only after a valid completion does it reset the Git baseline and mark the exact Phase 1 snapshots as consumed.&lt;/p&gt;

&lt;p&gt;The semantic result is model-authored. The scheduling, isolation, evidence diff, and acceptance checks are deterministic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Add, Update, Delete, And Forget Mean
&lt;/h2&gt;

&lt;p&gt;The word "delete" hides several different contracts in this system.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Who decides&lt;/th&gt;
&lt;th&gt;What actually happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add a rollout memory&lt;/td&gt;
&lt;td&gt;Phase 1 extraction model&lt;/td&gt;
&lt;td&gt;A new DB-backed raw memory and rollout summary are created after the idle rollout passes the signal gate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update a rollout memory&lt;/td&gt;
&lt;td&gt;Phase 1 extraction model + DB watermark&lt;/td&gt;
&lt;td&gt;A changed thread is re-extracted and its newer result replaces the previous row.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add or merge durable guidance&lt;/td&gt;
&lt;td&gt;Phase 2 consolidation model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MEMORY.md&lt;/code&gt;, &lt;code&gt;memory_summary.md&lt;/code&gt;, and possibly &lt;code&gt;skills/&lt;/code&gt; are created or rewritten from selected evidence.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove unsupported guidance&lt;/td&gt;
&lt;td&gt;Phase 2 consolidation model&lt;/td&gt;
&lt;td&gt;Deleted inputs in the Git diff cause evidence-scoped cleanup of consolidated files.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop an input from the active set&lt;/td&gt;
&lt;td&gt;Deterministic selection&lt;/td&gt;
&lt;td&gt;Low-use, stale, or displaced Phase 1 records stop appearing in &lt;code&gt;raw_memories.md&lt;/code&gt;; their rollout summary files are removed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete stale DB evidence&lt;/td&gt;
&lt;td&gt;Deterministic retention&lt;/td&gt;
&lt;td&gt;Unselected Phase 1 rows older than the unused-memory cutoff are deleted in bounded batches.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delete expired extension evidence&lt;/td&gt;
&lt;td&gt;Deterministic retention&lt;/td&gt;
&lt;td&gt;Timestamped extension resources older than seven days are removed before consolidation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forget a deleted thread&lt;/td&gt;
&lt;td&gt;Thread lifecycle code&lt;/td&gt;
&lt;td&gt;The thread's Phase 1 row and job are deleted; consolidation is enqueued if that evidence was selected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apply a user correction&lt;/td&gt;
&lt;td&gt;User request -&amp;gt; append-only note -&amp;gt; Phase 2 model&lt;/td&gt;
&lt;td&gt;With dedicated tools or separate write permission, the active agent records a small note asking to add, update, or delete information; consolidation applies it to generated outputs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forget everything&lt;/td&gt;
&lt;td&gt;User control&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;memory/reset&lt;/code&gt; or the debug clear command removes memory DB rows and the contents of memory directories while preserving chat threads and their memory-mode settings.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is stronger than "the model can edit a Markdown file." It is a lifecycle with several independent forgetting paths.&lt;/p&gt;

&lt;p&gt;It is also weaker than a transactional fact database. The consolidated handbook does not assign every claim a stable fact ID, confidence score, validity interval, or tombstone. The consolidation prompt asks the model to preserve provenance and uncertainty, but those properties live in Markdown structure and model compliance rather than a schema-enforced knowledge layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Authority Is Present, But Indirect
&lt;/h2&gt;

&lt;p&gt;The read-path prompt is the instruction block Codex injects into every session to tell the active model how to use memory: when to consult it, how to search it, and how to cite it. Its rules include a boundary on writing. It tells the active model that it may update memory only after an explicit direct request from the user. Even then, it should not directly edit &lt;code&gt;MEMORY.md&lt;/code&gt; or &lt;code&gt;memory_summary.md&lt;/code&gt;. When the dedicated memory tools are enabled, or the active permission profile separately permits the write, it can create one timestamped Markdown note under:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.codex/memories/extensions/ad_hoc/notes/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dedicated &lt;code&gt;add_ad_hoc_note&lt;/code&gt; tool enforces create-new semantics, a timestamped filename, a small path scope, and no overwrite. The extension instructions tell consolidation that every note is authoritative for memory content, including requests to add, edit, or delete remembered information. They also say never to delete the note itself and to treat its content as data rather than executable instructions.&lt;/p&gt;

&lt;p&gt;This creates a useful separation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user intent: append-only evidence
generated state: rewritable memory artifacts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The note is an inspectable, append-only correction artifact, but it is not a source-authenticated audit log: it contains the model's transcription rather than the original user message, identity, or source hash. The consolidation model decides how to reflect it in the generated memory hierarchy, and users must compare it with the originating conversation when exact attribution matters.&lt;/p&gt;

&lt;p&gt;There are still important limits. Dedicated memory tools are disabled by default even after the main feature is enabled, and the memory root is otherwise read-only to a normally managed &lt;code&gt;workspace-write&lt;/code&gt; agent. The background consolidation agent runs with no per-write approval. Users can inspect the plain files, toggle whether a chat can use or contribute memory, disable the feature, or reset the store, but there is no mandatory review queue for every inferred preference.&lt;/p&gt;

&lt;p&gt;For stable team rules, the official documentation therefore gives the right advice: keep them in &lt;code&gt;AGENTS.md&lt;/code&gt; or checked-in documentation. Memory is a recall layer, not the only copy of a requirement that must always govern behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Read Path Closes The Loop
&lt;/h2&gt;

&lt;p&gt;Codex does not embed every memory and retrieve nearest neighbors. It uses a staged file hierarchy.&lt;/p&gt;

&lt;p&gt;At thread start, the memory extension reads &lt;code&gt;memory_summary.md&lt;/code&gt;, truncates it to a fixed token budget, and injects it into developer instructions. That prompt tells the model to skip memory only for clearly self-contained tasks. For a relevant or ambiguous task, it should extract keywords from the summary, search &lt;code&gt;MEMORY.md&lt;/code&gt;, and open only one or two directly referenced skills or rollout summaries.&lt;/p&gt;

&lt;p&gt;This design makes retrieval partly deterministic and partly agentic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the summary is automatically visible;&lt;/li&gt;
&lt;li&gt;the model decides whether deeper memory is relevant;&lt;/li&gt;
&lt;li&gt;file search provides lexical retrieval;&lt;/li&gt;
&lt;li&gt;the model decides which evidence to open;&lt;/li&gt;
&lt;li&gt;citations expose which memory influenced the answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The citation path does more than improve observability. A citation has two parts. The file entries (&lt;code&gt;MEMORY.md:234-236&lt;/code&gt;, &lt;code&gt;rollout_summaries/...:10-12&lt;/code&gt;, &lt;code&gt;skills/...&lt;/code&gt;) point at the Markdown artifacts Phase 2 produced. The rollout IDs point back at the Phase 1 database records those artifacts were consolidated from. Codex parses those IDs from the final model output, increments &lt;code&gt;usage_count&lt;/code&gt;, and sets &lt;code&gt;last_usage&lt;/code&gt; for the corresponding Phase 1 records. Future Phase 2 selection then favors evidence that has actually helped later turns.&lt;/p&gt;

&lt;p&gt;That is a lightweight feedback loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;memory is selected -&amp;gt; model uses and cites it -&amp;gt; runtime records usage
-&amp;gt; frequently useful evidence remains eligible -&amp;gt; consolidation sees it again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is not reinforcement learning. No model weights are updated. It is runtime ranking and retention driven by model-produced citations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Codex Memory Is Not
&lt;/h2&gt;

&lt;p&gt;Several adjacent mechanisms can look like the same feature from the outside.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is deterministic instruction context. It is authored explicitly and should contain rules that must reliably apply. Codex memory is generated recall that may be incomplete, stale, or absent when the feature is disabled.&lt;/p&gt;

&lt;p&gt;Rollout storage is durable evidence. It lets Codex reopen or analyze what happened in one thread. A rollout becomes cross-session memory only after Phase 1 extracts it and Phase 2 selects and consolidates it.&lt;/p&gt;

&lt;p&gt;Compaction is short-term context management. It summarizes or replaces old model-visible history so one active thread can continue within a context limit. It does not decide that a lesson should influence unrelated future threads.&lt;/p&gt;

&lt;p&gt;These distinctions match the framework I used in &lt;a href="https://jczhu.com/blog/opencode-memory-internals/" rel="noopener noreferrer"&gt;OpenCode Memory Internals&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;instructions: what should govern behavior
history: what happened
working context: what the model can see now
long-term memory: what selected lessons should influence later sessions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Codex implements all four. OpenCode core currently implements the first three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codex Versus OpenCode
&lt;/h2&gt;

&lt;p&gt;The architectural difference is not that one tool writes files and the other does not. Both can write files. The difference is ownership.&lt;/p&gt;

&lt;p&gt;OpenCode loads &lt;code&gt;AGENTS.md&lt;/code&gt;, configured instruction sources, and durable session history. It compacts long sessions. Its &lt;code&gt;/init&lt;/code&gt; command can ask an active agent to create or improve project instructions. But core does not run a background policy that mines old sessions, promotes lessons, reconciles them with a global memory, and retrieves that memory in new sessions.&lt;/p&gt;

&lt;p&gt;Codex does. Its runtime owns eligibility, extraction jobs, a separate memories database, consolidation scheduling, usage accounting, retention, prompt injection, citations, and reset. Models supply the semantic judgments inside that lifecycle.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Codex local memory&lt;/th&gt;
&lt;th&gt;OpenCode core&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Write trigger&lt;/td&gt;
&lt;td&gt;Background processing of eligible idle rollouts&lt;/td&gt;
&lt;td&gt;Explicit user or agent file edit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic writer&lt;/td&gt;
&lt;td&gt;Phase 1 and Phase 2 models&lt;/td&gt;
&lt;td&gt;No dedicated memory writer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic update&lt;/td&gt;
&lt;td&gt;Re-extraction plus incremental consolidation&lt;/td&gt;
&lt;td&gt;No general cross-session update path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic forgetting&lt;/td&gt;
&lt;td&gt;Selection, retention, input deletion, and model cleanup&lt;/td&gt;
&lt;td&gt;No general cross-session forgetting policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Store&lt;/td&gt;
&lt;td&gt;SQLite evidence plus generated files under Codex home&lt;/td&gt;
&lt;td&gt;Instruction files plus session-scoped SQLite history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;Injected summary, lexical search, progressive file reads&lt;/td&gt;
&lt;td&gt;Deterministic instruction injection; no core cross-session semantic recall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedback&lt;/td&gt;
&lt;td&gt;Citations update usage and retention rank&lt;/td&gt;
&lt;td&gt;No equivalent core memory-usage loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User controls&lt;/td&gt;
&lt;td&gt;Per-chat use/contribute settings, config, inspection, reset&lt;/td&gt;
&lt;td&gt;Explicit instruction editing and session controls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OpenCode's simpler boundary is easier to audit. Codex's pipeline can reduce repeated steering. The price is a larger trust surface: old conversation content is transformed into future developer context by models running in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Other Memory Systems Put The Decision
&lt;/h2&gt;

&lt;p&gt;Comparing systems by storage technology alone is misleading. The more useful question is where memory policy lives.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Who decides memory operations?&lt;/th&gt;
&lt;th&gt;Update and forgetting model&lt;/th&gt;
&lt;th&gt;Retrieval model&lt;/th&gt;
&lt;th&gt;Primary design center&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;Background extraction and consolidation models inside a runtime-managed pipeline&lt;/td&gt;
&lt;td&gt;LLM rewrites plus deterministic retention and reset&lt;/td&gt;
&lt;td&gt;Prompt-loaded summary, lexical search, progressive disclosure&lt;/td&gt;
&lt;td&gt;Local coding-agent recall from prior work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode core&lt;/td&gt;
&lt;td&gt;User or active agent through ordinary instruction files&lt;/td&gt;
&lt;td&gt;Explicit file maintenance&lt;/td&gt;
&lt;td&gt;Deterministic instruction loading&lt;/td&gt;
&lt;td&gt;Transparent instructions and durable sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code auto memory&lt;/td&gt;
&lt;td&gt;Active Claude model during the session&lt;/td&gt;
&lt;td&gt;Model edits or deletes local Markdown; user can edit or delete it&lt;/td&gt;
&lt;td&gt;Bounded &lt;code&gt;MEMORY.md&lt;/code&gt; index plus on-demand topic reads&lt;/td&gt;
&lt;td&gt;Immediate per-repository learning from corrections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Letta Agent SDK&lt;/td&gt;
&lt;td&gt;The active agent edits git-backed MemFS; optional Dreaming launches background subagents&lt;/td&gt;
&lt;td&gt;Committed file revisions plus background consolidation when configured&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;system/&lt;/code&gt; files stay in context; other files appear as a tree and are read on demand&lt;/td&gt;
&lt;td&gt;Versioned, agent-owned memory that follows the agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mem0&lt;/td&gt;
&lt;td&gt;An extraction pipeline compares new messages with retrieved candidates&lt;/td&gt;
&lt;td&gt;LLM chooses &lt;code&gt;ADD&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, or no change&lt;/td&gt;
&lt;td&gt;Vector search within entity scope, with entity-aware ranking&lt;/td&gt;
&lt;td&gt;Application memory as a service or library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LangMem&lt;/td&gt;
&lt;td&gt;Developer-configured hot-path tools or background managers&lt;/td&gt;
&lt;td&gt;LLM transforms profiles or collections; store layer upserts/deletes&lt;/td&gt;
&lt;td&gt;Direct lookup, semantic search, and metadata filters&lt;/td&gt;
&lt;td&gt;Composable memory primitives for LangGraph applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graphiti / Zep&lt;/td&gt;
&lt;td&gt;Ingestion pipeline extracts entities, facts, and temporal relationships&lt;/td&gt;
&lt;td&gt;Superseded facts are invalidated with history preserved&lt;/td&gt;
&lt;td&gt;Hybrid semantic, keyword, and graph traversal&lt;/td&gt;
&lt;td&gt;Time-aware facts, relationships, and provenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AgeMem&lt;/td&gt;
&lt;td&gt;The task model itself selects memory actions as part of its learned policy&lt;/td&gt;
&lt;td&gt;Explicit Add, Update, Delete, Summary, and Filter actions&lt;/td&gt;
&lt;td&gt;Learned Retrieve action&lt;/td&gt;
&lt;td&gt;Joint long-term and short-term memory control through RL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three patterns stand out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent-Directed Memory
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2310.08560" rel="noopener noreferrer"&gt;MemGPT&lt;/a&gt; framed memory as an operating-system problem: move information between a small fast context and larger external storage. Letta's legacy V1 SDK expressed that lineage through editable memory blocks and archival tools. The current &lt;a href="https://docs.letta.com/agent-sdk/memory/" rel="noopener noreferrer"&gt;Letta Agent SDK&lt;/a&gt; instead uses git-backed MemFS: files under &lt;code&gt;system/&lt;/code&gt; remain in context, other files are exposed as an on-demand tree, and optional Dreaming subagents consolidate recent conversations in the background.&lt;/p&gt;

&lt;p&gt;The active agent can still edit its own persistent memory, while Dreaming moves some consolidation off the hot path. That combination illustrates that agent-directed and background memory do not have to be mutually exclusive.&lt;/p&gt;

&lt;p&gt;Claude Code's &lt;a href="https://code.claude.com/docs/en/memory" rel="noopener noreferrer"&gt;auto memory&lt;/a&gt; is also close to the hot path. Claude writes a compact &lt;code&gt;MEMORY.md&lt;/code&gt; index and topic files during the session. It can learn immediately from a correction, and users can inspect, edit, or delete the files. There is no separate delayed consolidation architecture comparable to Codex's two-phase pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Background Memory Pipelines
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.mem0.ai/core-concepts/memory-types" rel="noopener noreferrer"&gt;Mem0&lt;/a&gt; and &lt;a href="https://langchain-ai.github.io/langmem/concepts/conceptual_guide/" rel="noopener noreferrer"&gt;LangMem&lt;/a&gt; make the extraction pipeline an application primitive. New messages are compared with existing state, and an LLM chooses whether to insert, update, delete, or consolidate memories. LangMem explicitly supports both hot-path and background formation and distinguishes profiles, collections, episodes, and procedural prompt updates.&lt;/p&gt;

&lt;p&gt;Codex belongs in this family, but with a coding-agent-specific choice: it stores rich per-rollout evidence, consolidates into a human-readable handbook, and lets future models progressively disclose detail through file search rather than requiring a vector store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Temporal And Learned Policies
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/getzep/graphiti" rel="noopener noreferrer"&gt;Graphiti&lt;/a&gt; treats contradiction as a temporal data problem. Facts have validity windows, raw episodes preserve provenance, and superseded relationships are invalidated rather than erased. This is stronger than asking a Markdown-writing model to decide which sentence sounds current, especially when historical truth matters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2601.01885" rel="noopener noreferrer"&gt;AgeMem&lt;/a&gt; moves the decision one level deeper. Add, Update, Delete, Retrieve, Summary, and Filter are actions in the model's policy. A three-stage reinforcement-learning curriculum connects early storage decisions, short-term context control, and eventual task reward.&lt;/p&gt;

&lt;p&gt;Codex does not do this. Its models are guided by detailed prompts inside a fixed pipeline. Usage changes selection, but task outcomes do not train a memory policy. The distinction is between &lt;strong&gt;a model making memory judgments&lt;/strong&gt; and &lt;strong&gt;a model trained to make memory judgments&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trust Boundary Is The Real Architecture
&lt;/h2&gt;

&lt;p&gt;The most consequential Codex design choice is not Markdown versus vectors. It is the point where untrusted history can become future developer context.&lt;/p&gt;

&lt;p&gt;"Local" describes the store, not necessarily the processing boundary. Eligible chat content and path metadata are submitted to the configured model provider for extraction, and generated memory later returns in model requests. Secret redaction covers the serialized rollout input and generated fields, but it does not remove every piece of personal, proprietary, or path-identifying information.&lt;/p&gt;

&lt;p&gt;The implementation adds several defenses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Phase 1 tells the model to treat rollout text and tool output as data, not instructions.&lt;/li&gt;
&lt;li&gt;Generated fields pass through secret redaction before entering the memories database.&lt;/li&gt;
&lt;li&gt;An optional &lt;code&gt;disable_on_external_context&lt;/code&gt; setting marks threads that used web search, MCP, tool search, or similar external context as polluted and excludes them from generation.&lt;/li&gt;
&lt;li&gt;For Codex-managed permission profiles, Phase 2 has no network and writes only inside the memory root; explicitly disabled or externally enforced parent profiles are preserved instead.&lt;/li&gt;
&lt;li&gt;Consolidation threads are ephemeral and cannot recursively generate memories.&lt;/li&gt;
&lt;li&gt;Required artifacts and symlink safety are checked before a run is accepted.&lt;/li&gt;
&lt;li&gt;Retrieval requires citations, and unverified memory-derived facts should be described as potentially stale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The optional pollution guard deserves attention because it is off by default. When enabled, external context changes a thread's memory mode to &lt;code&gt;polluted&lt;/code&gt;. If the thread had already contributed selected evidence, Codex schedules consolidation so that evidence can be removed from the active memory set.&lt;/p&gt;

&lt;p&gt;This is an explicit answer to a prompt-injection question: should web pages and third-party tool output be allowed to influence cross-session memory? Codex lets the user choose a conservative answer, but does not make it the default.&lt;/p&gt;

&lt;p&gt;The remaining risks are structural. Secret redaction is not a proof that every sensitive fact is gone. Markdown provenance is not a cryptographic chain of custody. A no-approval consolidation agent can still infer an unstable preference or overgeneralize a one-off correction. Citation-based usage can reinforce a memory because models keep citing it, not necessarily because it is correct.&lt;/p&gt;

&lt;p&gt;Memory quality therefore cannot be reduced to recall rate. It needs authority, provenance, correction, and deletion semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Runtime Builders Should Copy
&lt;/h2&gt;

&lt;p&gt;The first lesson is to split semantic policy from lifecycle policy. Let models judge meaning, but let deterministic code bound when they run, what evidence they receive, where they can write, and how failures are retried.&lt;/p&gt;

&lt;p&gt;The second lesson is to make deletion evidence-driven. Codex does not merely tell a model to "keep memory fresh." It presents a Git diff that includes removed inputs and asks the model to delete only claims whose support disappeared. That is still probabilistic, but it gives forgetting a concrete cause.&lt;/p&gt;

&lt;p&gt;The third lesson is to keep the read path bounded. Always loading a small routing index and progressively opening detail is easier to inspect and budget than injecting an unbounded profile or retrieving a large opaque set of nearest neighbors.&lt;/p&gt;

&lt;p&gt;The fourth lesson is to feed usage back into retention without confusing usage with truth. Citations provide a practical relevance signal. They should not become the only quality signal, because repeated retrieval can create a self-reinforcing mistake.&lt;/p&gt;

&lt;p&gt;The fifth lesson is to keep hard rules outside learned memory. A test command that must run before release belongs in &lt;code&gt;AGENTS.md&lt;/code&gt;. A preference inferred from several prior interactions may belong in generated memory. The system should not require a probabilistic promotion pipeline to rediscover mandatory policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Map
&lt;/h2&gt;

&lt;p&gt;The Codex claims above come primarily from these files in the pinned source tree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;codex-rs/features/src/lib.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/config/src/types.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/app-server/src/request_processors/turn_processor.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/memories/write/src/start.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/memories/write/src/phase1.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/memories/write/src/phase2.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/memories/write/src/storage.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/memories/write/src/workspace.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/memories/write/templates/memories/stage_one_system.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/memories/write/templates/memories/consolidation.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/ext/memories/templates/memories/read_path.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/state/src/runtime/memories.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/ext/memories/src/extension.rs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codex-rs/ext/memories/src/local/ad_hoc_note.rs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product-level behavior and controls are documented in &lt;a href="https://developers.openai.com/codex/customization/memories.md" rel="noopener noreferrer"&gt;Codex Memories&lt;/a&gt;. The comparison uses the primary project documentation and papers linked in the relevant sections above.&lt;/p&gt;

&lt;p&gt;I expected to find either a simple memory tool or a conventional extract-and-vector-search pipeline. Codex implements something more opinionated: a delayed two-stage editorial process over local evidence, with a Git diff acting as the bridge between deterministic retention and model-driven rewriting.&lt;/p&gt;

&lt;p&gt;That architecture answers the original question. The LLM does decide what Codex remembers, but it does not decide alone. Runtime code determines which experiences are eligible to become evidence, which evidence remains economically viable, and when the model must reconcile deletion. Reliable memory comes from that division of authority, not from granting a model unrestricted access to a persistent file.&lt;/p&gt;

</description>
      <category>coding</category>
      <category>codex</category>
      <category>opencode</category>
      <category>agents</category>
    </item>
    <item>
      <title>OpenCode Memory Internals</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Tue, 25 Aug 2026 06:23:29 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/opencode-memory-internals-2p8g</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/opencode-memory-internals-2p8g</guid>
      <description>&lt;p&gt;I started this investigation after finding a local OpenCode project that appeared to remember guidance across sessions. The guidance lived in Markdown files outside the repository, yet every new session followed it. The operational question was simple: had OpenCode decided to preserve those facts, or had someone explicitly written them?&lt;/p&gt;

&lt;p&gt;The session record answered it. An agent had created the files through an ordinary file tool after an explicit user request. A project-local &lt;code&gt;instructions&lt;/code&gt; configuration then loaded them on every provider turn. What looked like autonomous memory was user-triggered file authoring plus deterministic prompt injection.&lt;/p&gt;

&lt;p&gt;That result sent me looking for the actual memory subsystem. There is no general runtime-managed service that decides what to save, updates facts when they change, and semantically retrieves useful knowledge in later sessions.&lt;/p&gt;

&lt;p&gt;What users experience as "memory" is produced by three different mechanisms with different owners and failure modes: instruction files are loaded into the system prompt, durable session events and projected messages are persisted in SQLite, and old model-visible context is replaced by a generated compaction checkpoint when the request grows too large. These mechanisms work together, but they do not form an autonomous long-term memory manager.&lt;/p&gt;

&lt;p&gt;That distinction matters. If a coding agent remembers a project rule because &lt;code&gt;AGENTS.md&lt;/code&gt; is injected on every turn, that is not learned memory. If it can reopen an old transcript from SQLite, that does not mean a new session can retrieve facts from it. If a long session survives by summarizing its history, that does not mean the runtime selected the most important information.&lt;/p&gt;

&lt;p&gt;This article follows the current OpenCode source tree, which contains both the desktop-compatible session path under &lt;code&gt;packages/opencode&lt;/code&gt; and the newer V2 runtime under &lt;code&gt;packages/core&lt;/code&gt;. Where the two paths differ, I call out the difference rather than treating them as one implementation.&lt;/p&gt;

&lt;p&gt;Primary code references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/instruction.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/prompt.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/llm/request.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/system.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/prompt/beast.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/tool/read.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/compaction.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/message-v2.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/command/index.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/command/template/initialize.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/instruction-context.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/context-epoch.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/runner/llm.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/history.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/sql.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/compaction.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/database/database.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OpenCode does not have one memory system. It has an explicit instruction layer, a durable session layer, and a bounded working-context layer.&lt;/strong&gt; Calling all three "memory" hides the most important operational differences.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Project knowledge is mostly prompt injection.&lt;/strong&gt; &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, and configured &lt;code&gt;instructions&lt;/code&gt; are read from files and placed in the system prompt. The loader reads them; it does not maintain them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session persistence is not cross-session recall.&lt;/strong&gt; OpenCode durably stores messages, tool calls, tool results, and durable events in SQLite. Streaming text, reasoning, tool-input, and compaction deltas are transient. The stored data makes sessions reopenable and inspectable, but it remains scoped to the session unless another component explicitly reads it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compaction changes what the model sees, not what the database retains.&lt;/strong&gt; Old history remains durable, while normal model requests continue from a generated checkpoint plus recent context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenCode does not have a general runtime policy that promotes conversation facts into long-term memory.&lt;/strong&gt; There is no built-in embedding index, vector retrieval path, user-profile store, or fact confidence model. One provider-specific prompt contains a narrow file-memory convention, but core does not manage or retrieve that file as a memory service.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;/init&lt;/code&gt; is guided instruction authoring, not background learning.&lt;/strong&gt; It asks the active agent to create or improve &lt;code&gt;AGENTS.md&lt;/code&gt; after the user invokes the command.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The legacy and V2 instruction paths are not identical.&lt;/strong&gt; The desktop-compatible path supports &lt;code&gt;AGENTS.md&lt;/code&gt;, optional &lt;code&gt;CLAUDE.md&lt;/code&gt;, configured local files, and HTTP instruction sources. The current V2 &lt;code&gt;InstructionContext&lt;/code&gt; observes &lt;code&gt;AGENTS.md&lt;/code&gt; files only.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A Better Mental Model
&lt;/h2&gt;

&lt;p&gt;The easiest way to reason about OpenCode memory is to ask two questions for every piece of information:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where is it stored?&lt;/li&gt;
&lt;li&gt;Is it included in the next model request?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those are not the same question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Stored in&lt;/th&gt;
&lt;th&gt;How it reaches the model&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ambient instructions&lt;/td&gt;
&lt;td&gt;Markdown files or configured URLs&lt;/td&gt;
&lt;td&gt;Injected as system context&lt;/td&gt;
&lt;td&gt;Project or configured scope, across sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable session history&lt;/td&gt;
&lt;td&gt;SQLite messages, parts, inputs, and durable events&lt;/td&gt;
&lt;td&gt;Selected history is converted into model messages&lt;/td&gt;
&lt;td&gt;One session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction checkpoint&lt;/td&gt;
&lt;td&gt;A durable summary message or compaction event&lt;/td&gt;
&lt;td&gt;Replaces older active history in later requests&lt;/td&gt;
&lt;td&gt;One long session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The request path is approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent system prompt
+ ambient instructions
+ selected session history
+ latest user input
+ tool definitions
-&amp;gt; provider request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database may contain much more than the provider request. A project may also contain instructions that are reloaded independently of the transcript. "The runtime has it" and "the model can currently reason over it" are separate properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer One: Ambient Instructions
&lt;/h2&gt;

&lt;p&gt;In the desktop-compatible path, instruction discovery lives in &lt;code&gt;packages/opencode/src/session/instruction.ts&lt;/code&gt;. It collects several classes of sources.&lt;/p&gt;

&lt;p&gt;The global source is an &lt;code&gt;AGENTS.md&lt;/code&gt; under the OpenCode config directory, with optional compatibility loading for &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;. At project level, it searches upward for instruction files. &lt;code&gt;AGENTS.md&lt;/code&gt; is preferred, followed by &lt;code&gt;CLAUDE.md&lt;/code&gt;, then the deprecated &lt;code&gt;CONTEXT.md&lt;/code&gt;; the first filename family with matches wins. The project configuration may also add &lt;code&gt;instructions&lt;/code&gt; entries that resolve to local paths, glob patterns, or HTTP URLs.&lt;/p&gt;

&lt;p&gt;The loader reads those sources and renders each one with its origin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Instructions from: /path/to/AGENTS.md
&amp;lt;file content&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SessionPrompt&lt;/code&gt; calls &lt;code&gt;instruction.system()&lt;/code&gt; while preparing each provider step. It combines the resulting text with environment context, MCP instructions, and skills. &lt;code&gt;LLMRequestPrep.prepare(...)&lt;/code&gt; then joins that material with the selected agent prompt and any per-user system content before creating provider system messages.&lt;/p&gt;

&lt;p&gt;This is simple and strong. A project rule does not depend on the model remembering a conversation from last week. It is supplied again as authoritative context. Editing the file changes the context future turns receive.&lt;/p&gt;

&lt;p&gt;It is also important not to overstate what happens. The instruction loader has read, discovery, and fetch behavior. It has no code that decides a new lesson is worth preserving, rewrites a stale rule, or deletes a contradiction. A human or an agent using ordinary file tools must make those edits.&lt;/p&gt;

&lt;p&gt;OpenCode adds one useful directory-sensitive behavior. When the &lt;code&gt;read&lt;/code&gt; tool opens a source file, the runtime can discover a nearer instruction file between that file and the project root and attach it to the tool result as a system reminder. A monorepo can therefore keep broad rules at the root and narrower rules close to a package. This is deterministic path lookup, not semantic retrieval.&lt;/p&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;/init&lt;/code&gt; Actually Does
&lt;/h3&gt;

&lt;p&gt;The built-in &lt;code&gt;/init&lt;/code&gt; command is the closest OpenCode gets to authoring persistent project memory. Its command description is "guided AGENTS.md setup," and its prompt asks the agent to investigate the repository and create or update &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The prompt is carefully scoped. It tells the agent to inspect manifests, CI, developer commands, existing instructions, and representative architecture files. It asks for high-signal facts that future sessions would otherwise miss, and it explicitly rejects generic advice and unverified claims.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;/init&lt;/code&gt; is user-triggered. It does not run at the end of every session or perform a dedicated transcript-mining pass for newly learned facts. It executes within the normal active-session context and uses the normal agent and file-writing tools to maintain one explicit instruction artifact.&lt;/p&gt;

&lt;p&gt;That makes &lt;code&gt;/init&lt;/code&gt; closer to generating repository documentation than to an always-on memory policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Narrow Provider-Prompt Exception
&lt;/h3&gt;

&lt;p&gt;There is one built-in convention that prevents a completely categorical "no memory" claim. For model API IDs containing &lt;code&gt;gpt-4&lt;/code&gt;, &lt;code&gt;o1&lt;/code&gt;, or &lt;code&gt;o3&lt;/code&gt;, &lt;code&gt;packages/opencode/src/session/system.ts&lt;/code&gt; selects &lt;code&gt;prompt/beast.txt&lt;/code&gt;. That provider prompt tells the agent that user preferences may be stored in &lt;code&gt;.github/instructions/memory.instruction.md&lt;/code&gt; and that, when the user explicitly asks it to remember something, it may create or update that file.&lt;/p&gt;

&lt;p&gt;This is a prompt-level file convention, not a runtime memory service. The normal instruction loader does not search for &lt;code&gt;memory.instruction.md&lt;/code&gt;. Core does not index the file, reconcile contradictions, attach confidence or provenance, or automatically retrieve it across sessions. The model can use ordinary file tools to write or read it if the prompt and current task lead it there.&lt;/p&gt;

&lt;p&gt;The distinction is useful: OpenCode ships a provider-specific instruction that suggests a memory behavior, but not a provider-independent subsystem that owns memory state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The V2 Instruction Path
&lt;/h2&gt;

&lt;p&gt;The newer core runtime makes ambient context more explicit. &lt;code&gt;packages/core/src/instruction-context.ts&lt;/code&gt; registers instructions as a typed &lt;code&gt;SystemContext&lt;/code&gt; source. It observes the global config &lt;code&gt;AGENTS.md&lt;/code&gt; and project &lt;code&gt;AGENTS.md&lt;/code&gt; files between the active directory and project root, then renders them into an instruction baseline.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SessionContextEpoch&lt;/code&gt; persists both the rendered baseline and a structured snapshot for the session. Before a provider turn, &lt;code&gt;SessionRunner&lt;/code&gt; prepares the current system context and sends the selected agent system prompt plus the baseline to the model. If the observed instruction state changes, the context system can produce an explicit replacement update rather than silently mixing old and new versions.&lt;/p&gt;

&lt;p&gt;This is a better model for a durable agent runtime: system context has identity, a baseline, and update semantics.&lt;/p&gt;

&lt;p&gt;It is not yet feature-equivalent to the desktop-compatible loader. The current V2 &lt;code&gt;InstructionContext&lt;/code&gt; searches for &lt;code&gt;AGENTS.md&lt;/code&gt;; it does not wire the optional &lt;code&gt;CLAUDE.md&lt;/code&gt; compatibility source or the local and remote paths declared by the legacy &lt;code&gt;instructions&lt;/code&gt; configuration. Anyone migrating behavior between the two runtimes should verify the active path instead of assuming that "OpenCode loads instructions" means the same thing everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer Two: Durable Session History
&lt;/h2&gt;

&lt;p&gt;OpenCode's second memory-like mechanism is session persistence.&lt;/p&gt;

&lt;p&gt;The SQLite database runs in WAL mode and normally lives under OpenCode's data directory as &lt;code&gt;opencode.db&lt;/code&gt;, with channel-specific filenames available for development builds. The schema contains both the desktop-compatible message projection and the newer event-oriented session tables.&lt;/p&gt;

&lt;p&gt;The desktop-compatible path stores &lt;code&gt;message&lt;/code&gt; rows and separate &lt;code&gt;part&lt;/code&gt; rows. A message is not just text. Parts represent text, reasoning, files, tool calls, tool results, step boundaries, snapshots, patches, errors, and compaction markers. The session processor updates these records while the provider stream and tools are running.&lt;/p&gt;

&lt;p&gt;The V2 path adds durable session inputs, ordered durable events, projected &lt;code&gt;session_message&lt;/code&gt; rows, and a context epoch. Prompt admission and model execution are separated. Durable lifecycle events describe what happened; projectors create query-friendly messages; the runner reloads selected history before the next provider turn. High-frequency text, reasoning, tool-input, and compaction delta events remain transient rather than being inserted into the durable event stream.&lt;/p&gt;

&lt;p&gt;This persistence gives OpenCode several properties commonly mistaken for long-term memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a session survives process restart;&lt;/li&gt;
&lt;li&gt;the UI can reopen old conversations;&lt;/li&gt;
&lt;li&gt;tool execution can be inspected after completion;&lt;/li&gt;
&lt;li&gt;clients can rebuild projections from durable state;&lt;/li&gt;
&lt;li&gt;debugging can refer to stable session, message, part, and tool-call identities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But persistence alone does not create recall. A new session does not search every old session for facts about the user or repository. There is no built-in retrieval query such as "find prior decisions relevant to this prompt." Unless content has been moved into an instruction file or supplied by a plugin, old session data remains old session data.&lt;/p&gt;

&lt;p&gt;This boundary is worth preserving. Durable history is an audit and recovery substrate. Cross-session memory is a selection and trust problem. Combining them implicitly would make every old statement a candidate instruction, including mistakes, obsolete plans, secrets, and prompt-injected content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer Three: Bounded Working Context
&lt;/h2&gt;

&lt;p&gt;Even within one session, the model cannot see an unlimited transcript. Tool outputs, file reads, reasoning, and repeated provider turns eventually exceed the model's context window.&lt;/p&gt;

&lt;p&gt;OpenCode handles that with compaction.&lt;/p&gt;

&lt;p&gt;The desktop-compatible implementation estimates the active history and retains a recent tail within a configured budget. For the older head sent to the summarizer, it strips media and truncates large tool outputs, then asks a dedicated compaction agent to generate a summary. It persists that summary as an assistant summary message. &lt;code&gt;filterCompacted(...)&lt;/code&gt; later reorders model-visible history so the provider sees the compaction request, its summary, the retained tail, and subsequent work instead of the full original transcript.&lt;/p&gt;

&lt;p&gt;The same path has a separate optional pruning mechanism. When &lt;code&gt;compaction.prune&lt;/code&gt; is enabled, it scans older completed tool outputs while protecting a recent token budget. Outputs selected for pruning remain in SQLite but are marked compacted; later model-message conversion replaces their content with &lt;code&gt;[Old tool result content cleared]&lt;/code&gt; and drops their attachments. This frees model context without creating a new summary checkpoint. It is disabled unless configured and should not be confused with history summarization.&lt;/p&gt;

&lt;p&gt;The V2 implementation uses a durable checkpoint-and-retry design. When a request estimate crosses the context threshold, or the provider reports overflow before assistant output has started, it serializes older history, keeps recent context, generates a bounded summary, persists a compaction event, and retries from the new checkpoint. Future active-history queries begin at the latest compaction boundary. Older rows remain in SQLite but are no longer part of normal provider requests.&lt;/p&gt;

&lt;p&gt;The three layers are deliberately separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;durable history: retained
active history: shortened
model-visible history: summary checkpoint + recent context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is continuity under a hard context limit, not intelligent forgetting. The trigger is size, not relevance. The summarizer may preserve objectives, files, errors, and next actions, but there is no policy that scores every message by future utility. A detail omitted from the checkpoint still exists in storage, yet the model will not recover it through the normal continuation path.&lt;/p&gt;

&lt;p&gt;I covered the V2 checkpoint algorithm and its limits in more detail in &lt;a href="https://jczhu.com/blog/opencode-v2-compaction-internals/" rel="noopener noreferrer"&gt;OpenCode V2 Compaction Internals&lt;/a&gt;. For the memory model, the central point is simpler: compaction is a lossy projection over durable history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenCode Does Not Implement
&lt;/h2&gt;

&lt;p&gt;After tracing these paths, several negative claims are as important as the positive ones.&lt;/p&gt;

&lt;p&gt;OpenCode does not currently provide a general runtime-managed autonomous memory store with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;embedding generation and vector search over remembered facts;&lt;/li&gt;
&lt;li&gt;cross-session retrieval based on semantic similarity;&lt;/li&gt;
&lt;li&gt;provider-independent &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, or &lt;code&gt;forget&lt;/code&gt; memory tools;&lt;/li&gt;
&lt;li&gt;automatic extraction of user preferences or repository decisions;&lt;/li&gt;
&lt;li&gt;confidence, provenance, contradiction, expiration, or access-control metadata for facts;&lt;/li&gt;
&lt;li&gt;a background policy that promotes session content into long-term memory;&lt;/li&gt;
&lt;li&gt;reinforcement learning that teaches the model when to store or retrieve information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Provider SDK types may mention embeddings, file search, or vector stores. Plugins may inject external context or replace compaction behavior. Neither means OpenCode core owns a semantic memory subsystem.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;beast.txt&lt;/code&gt; convention also does not change this conclusion. It tells a subset of models that they may edit a particular file after an explicit request. It does not provide automatic extraction, loading, semantic retrieval, conflict resolution, or lifecycle management for the resulting content.&lt;/p&gt;

&lt;p&gt;A user can build a useful approximation by keeping Markdown outside a public repository and referencing it through project-local &lt;code&gt;instructions&lt;/code&gt;. That produces private, cross-session context without modifying upstream files. It is still user-maintained prompt context. OpenCode reads it because the configuration points to it; OpenCode does not decide what belongs there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why The Simpler Design Is Defensible
&lt;/h2&gt;

&lt;p&gt;It is tempting to treat the missing autonomous layer as an obvious gap. The trade-off is more complicated.&lt;/p&gt;

&lt;p&gt;Instruction files are transparent. They can be reviewed in a normal editor, versioned when appropriate, scoped by directory, and corrected without inspecting an opaque index. They are strong context because they enter the system prompt deliberately.&lt;/p&gt;

&lt;p&gt;Session persistence is auditable. It records what happened without silently turning every conversation into future policy. A user can delete, archive, inspect, or export sessions without also reasoning about which facts were extracted into another store.&lt;/p&gt;

&lt;p&gt;Summary compaction is bounded and observable. It has real information-loss risk, but it creates an explicit checkpoint instead of silently dropping arbitrary history. Optional tool-output pruning follows a different path: it leaves a durable compacted marker and substitutes a visible placeholder in later model context.&lt;/p&gt;

&lt;p&gt;An autonomous memory layer would need answers to harder questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which statements are facts, plans, preferences, or temporary hypotheses?&lt;/li&gt;
&lt;li&gt;Who is allowed to write a persistent fact?&lt;/li&gt;
&lt;li&gt;How is a contradiction resolved?&lt;/li&gt;
&lt;li&gt;When does a repository change make an old architectural fact stale?&lt;/li&gt;
&lt;li&gt;Can untrusted file content become cross-session memory?&lt;/li&gt;
&lt;li&gt;How can a user inspect why a memory was retrieved?&lt;/li&gt;
&lt;li&gt;What does deletion mean when memories have been summarized or duplicated?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a coding agent, these are safety and provenance questions, not only retrieval-quality questions. A wrong remembered API contract can cause a bad patch. A remembered secret can leak into an unrelated request. A stale build instruction can waste every future session.&lt;/p&gt;

&lt;p&gt;OpenCode's current architecture avoids those risks by keeping durable rules explicit and durable transcripts session-scoped. The cost is that users and agents must deliberately promote important knowledge into &lt;code&gt;AGENTS.md&lt;/code&gt; or another configured instruction source.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Agent Runtime Builders Should Copy
&lt;/h2&gt;

&lt;p&gt;The first lesson is to stop using "memory" as one undifferentiated feature name. Define at least three contracts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;instruction context: what should govern behavior
durable history: what happened
working context: what the model can see now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each contract needs different storage, authority, lifecycle, and failure handling.&lt;/p&gt;

&lt;p&gt;The second lesson is that durable storage and model visibility should remain separate. Keeping the full transcript is valuable for audit and recovery. Sending the full transcript forever is impossible. Compaction should create an explicit boundary so the runtime can explain what remains durable and what remains visible.&lt;/p&gt;

&lt;p&gt;The third lesson is to treat instruction mutation as a privileged operation. OpenCode's loader is read-only. Even &lt;code&gt;/init&lt;/code&gt; requires user action and uses ordinary file tools. If a future memory subsystem writes persistent context automatically, it should preserve source, timestamp, confidence, scope, and a review path rather than quietly editing the agent's future system prompt.&lt;/p&gt;

&lt;p&gt;The final lesson is that a simple file can be a good memory primitive when the requirement is stable project guidance. It is inspectable, portable, and deterministic. Vector retrieval and learned memory policies solve different problems. Adding them should start from a concrete failure that files, session history, and compaction cannot solve.&lt;/p&gt;

&lt;p&gt;OpenCode remembers less than the word "memory" suggests, but its boundaries are useful. Project instructions say what should remain true. SQLite records what happened. Compaction decides what can still fit. None of them autonomously decides what deserves to become a long-term fact.&lt;/p&gt;

&lt;p&gt;That is the core architectural claim: reliable agent memory begins by separating authority, durability, and visibility, not by adding a vector database.&lt;/p&gt;

&lt;p&gt;I also maintain an &lt;a href="https://jczhu.com/opencode-tools/" rel="noopener noreferrer"&gt;OpenCode Reliability Toolkit&lt;/a&gt; for readiness checks, database maintenance, session reflection, and multi-machine coordination.&lt;/p&gt;

</description>
      <category>coding</category>
      <category>memory</category>
      <category>ai</category>
      <category>opencode</category>
    </item>
    <item>
      <title>The Agent Session Is the New Log File</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Wed, 19 Aug 2026 02:48:05 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/the-agent-session-is-the-new-log-file-397c</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/the-agent-session-is-the-new-log-file-397c</guid>
      <description>&lt;p&gt;I kept seeing the same failures in different clothes. An agent would finish a task without running the right verification. Another session would drift because my first instruction was too vague. A third would reveal a missing tool primitive only after the agent had faked it badly with shell glue. None of these incidents were mysterious while they were happening. The mystery was why I kept rediscovering them one session at a time.&lt;/p&gt;

&lt;p&gt;The evidence was already there. OpenCode had the full conversation: what I asked, what the agent assumed, which tools it called, where it stopped, where I corrected it, and which mistake repeated from last week. But after a task ended, that evidence mostly turned back into scrollback. I might remember the pain, maybe write one rule into &lt;code&gt;AGENTS.md&lt;/code&gt;, maybe not. The session itself disappeared from the engineering process.&lt;/p&gt;

&lt;p&gt;That started to feel wrong. Code gets reviewed. Logs get searched. CI failures get archived. Production incidents get postmortems. But the conversation that produced the code, the test run, the bad deployment check, or the missing verification step is often treated as disposable. For AI-assisted development, that conversation is not disposable. It is the log of the human-agent system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part we do not review
&lt;/h2&gt;

&lt;p&gt;When a pull request goes wrong, I can inspect the diff. When a deployment goes wrong, I can inspect logs and metrics. When a test fails, I can inspect the failing command and output. Those artifacts are boring, durable, and searchable. They give the next debugging pass something firmer than memory.&lt;/p&gt;

&lt;p&gt;Agent sessions are different. They are full of useful evidence, but they are rarely treated as an artifact worth reviewing. The usual review stops at the code: did the patch compile, did the test pass, did the diff look reasonable? That misses a layer above the code, where many AI failures actually begin.&lt;/p&gt;

&lt;p&gt;The instruction may have been underspecified. The agent may have edited before reading enough context. The task may have needed a global search, but the conversation only named one file. The verification boundary may have been wrong: the agent checked that a command returned success, not that the returned data meant what success was supposed to mean. The tool gap may have been obvious in hindsight because the agent kept recreating the same fragile shell loop, but no one stopped to ask whether that loop should become a real primitive.&lt;/p&gt;

&lt;p&gt;These are not code-review findings. They are session-review findings. You only see them by reading the conversation as an execution trace.&lt;/p&gt;




&lt;h2&gt;
  
  
  A session is telemetry
&lt;/h2&gt;

&lt;p&gt;Once I started thinking of sessions as telemetry, the useful question changed. It was no longer: was this answer good? That question is too local. A single answer can be fine while the workflow around it is broken.&lt;/p&gt;

&lt;p&gt;The better question is: what keeps happening?&lt;/p&gt;

&lt;p&gt;If I correct the same instruction three times, that is not a prompt problem anymore. It is a missing rule. If an agent repeatedly invents a polling loop with &lt;code&gt;sleep&lt;/code&gt; and &lt;code&gt;curl&lt;/code&gt;, that is not a one-off bash mistake. It is a missing tool. If every large task ends with me saying, "you should have checked the rest of the codebase too," that is not an unlucky review comment. It is a missing checklist step.&lt;/p&gt;

&lt;p&gt;This is the same move engineers already make everywhere else. One failed request is an error. A pattern of failed requests is an SLO problem. One flaky test is annoying. A cluster of flaky tests is a signal about architecture, isolation, or ownership. One bad agent session is just a bad session. A repeated agent-session shape is workflow telemetry.&lt;/p&gt;

&lt;p&gt;The hard part is that humans are bad at remembering this evidence. I remember the emotional contour of a session much better than the actual sequence of events. I remember that I got frustrated. I do not reliably remember whether the first mistake was my instruction, the agent's shortcut, a missing tool, or a verification step that looked complete but was aimed at the wrong thing. If I want to improve the workflow instead of just complain about it, I need the transcript.&lt;/p&gt;




&lt;h2&gt;
  
  
  The plugin
&lt;/h2&gt;

&lt;p&gt;So I wrote &lt;a href="https://github.com/chncaesar/opencode-session-reflection" rel="noopener noreferrer"&gt;&lt;code&gt;opencode-session-reflection&lt;/code&gt;&lt;/a&gt;, a small OpenCode plugin that adds one tool: &lt;code&gt;session_reflection&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The installation path is intentionally boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"opencode-session-reflection"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart OpenCode, then ask it to review recent sessions. The registered &lt;code&gt;session_reflection&lt;/code&gt; tool is the primary interface; &lt;code&gt;/session-review&lt;/code&gt; is only an optional local-development helper. The plugin reads session metadata and transcripts through OpenCode APIs, with cross-project discovery through &lt;code&gt;/experimental/session&lt;/code&gt;; it never accesses SQLite directly.&lt;/p&gt;

&lt;p&gt;The analysis prompt focuses on three categories.&lt;/p&gt;

&lt;p&gt;First: developer-to-agent communication gaps. Was the task framed poorly? Was the scope ambiguous? Did I fail to say whether I wanted discussion or implementation? Did I omit acceptance criteria or verification requirements that I actually knew at the time?&lt;/p&gt;

&lt;p&gt;Second: recurring OpenCode mistakes. Did the agent edit before reading context? Did it claim completion without verification? Did it fix one reported occurrence without searching for the same pattern elsewhere? Did it ignore project rules, overbuild, or stop early?&lt;/p&gt;

&lt;p&gt;Third: plugin, skill, command, or rule opportunities. This is the most important category for me. The goal is not to turn every annoyance into automation. The goal is to find repeated friction where OpenCode can observe a reliable trigger and take a safe action. Some failures should become a rule. Some should become a skill. Some should become a slash command. A few deserve a plugin.&lt;/p&gt;

&lt;p&gt;The plugin writes selected session ids, hashed directory paths, message counts, transcript counts, tool-call counts, prompt hashes, and saved report paths under the OpenCode config directory. Redacted audit metadata stays local and is not uploaded by the plugin. It does not store raw transcripts or session titles in the audit manifest. Selected session evidence may reach the model provider configured in OpenCode.&lt;/p&gt;

&lt;p&gt;Saved Markdown reports are different from the redacted audit manifest: they contain the supplied analysis and may preserve excerpts selected by the model. Saved Markdown reports may contain sensitive excerpts; users control their retention and deletion. Treat both reports and local audit metadata as private.&lt;/p&gt;

&lt;p&gt;That boundary matters. A session-review tool should not quietly become a second telemetry product. The point is to help the developer inspect their own local workflow, not to upload their mistakes somewhere else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this belongs in the plugin ecosystem
&lt;/h2&gt;

&lt;p&gt;The obvious use is personal: run it on the last few sessions and see what you keep doing wrong. That is already useful. But the more interesting use is upstream of plugin design.&lt;/p&gt;

&lt;p&gt;I recently wrote &lt;a href="https://github.com/chncaesar/opencode-waitfor" rel="noopener noreferrer"&gt;&lt;code&gt;opencode-waitfor&lt;/code&gt;&lt;/a&gt; because I kept watching agents fake readiness checks with brittle shell loops. The pattern was concrete. The tool boundary was clear. A URL, port, or command should be polled until a condition holds or a timeout returns the last observed state. That is a good plugin because the repeated behavior was observable and the replacement primitive was smaller than the broken behavior it displaced.&lt;/p&gt;

&lt;p&gt;But not every repeated failure is that clean. Some are better handled by an &lt;code&gt;AGENTS.md&lt;/code&gt; rule. Some need a skill because the real work is reasoning discipline, not API access. Some only need a slash command that packages a known sequence of prompts and tool calls. Some should not be automated at all because the trigger is too ambiguous or the action is too risky.&lt;/p&gt;

&lt;p&gt;That is why I wanted the reflection step to ask about feasibility and value, not just annoyance. Can OpenCode detect the situation without brittle transcript parsing? Is the required data available through the SDK? What are the false positives? Does an existing plugin or command already cover most of the need? Would this help other OpenCode users, or is it just a fossil from my private workflow?&lt;/p&gt;

&lt;p&gt;Good agent tooling should come from repeated evidence, not imagination. Otherwise it is too easy to build impressive little tools for problems that occurred once, or tools that automate the part that should have stayed under human approval. A session review is a filter. It turns "that was annoying" into "this happened four times, the trigger is visible, the safe action is narrow, and the value is real."&lt;/p&gt;




&lt;h2&gt;
  
  
  Start with one repeated failure
&lt;/h2&gt;

&lt;p&gt;If you use OpenCode, the useful experiment is small. Install the plugin, run a review over your last few sessions, and look for one repeated failure. Not ten. One.&lt;/p&gt;

&lt;p&gt;Maybe it is a communication habit: you keep sending underspecified requests and then correcting the agent two rounds later. Maybe it is a verification habit: the agent keeps checking that commands ran, not that the right thing changed. Maybe it is a tool gap: the agent keeps synthesizing the same fragile shell pattern because no better primitive exists. Pick the clearest one and decide what kind of artifact it deserves.&lt;/p&gt;

&lt;p&gt;If it applies across future sessions, write a rule. If it is a reasoning workflow, write a skill. If it is a repeated user-invoked flow, write a command. If it is a narrow, observable operation that agents keep faking badly, write a plugin.&lt;/p&gt;

&lt;p&gt;The point is not to make the agent introspective. The point is to make the workflow observable. Once the session becomes a log, repeated mistakes stop being anecdotes. They become engineering input.&lt;/p&gt;

</description>
      <category>opencode</category>
      <category>ai</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>db-semantic-mcp Gives AI Agents a Safe Semantic Map of Your Database</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Tue, 11 Aug 2026 00:38:27 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/db-semantic-mcp-gives-ai-agents-a-safe-semantic-map-of-your-database-2j8i</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/db-semantic-mcp-gives-ai-agents-a-safe-semantic-map-of-your-database-2j8i</guid>
      <description>&lt;p&gt;AI agents are getting database access before they understand databases.&lt;/p&gt;

&lt;p&gt;That is the wrong order.&lt;/p&gt;

&lt;p&gt;A real production database is rarely self-explanatory. The important table is not always named &lt;code&gt;orders&lt;/code&gt;. The customer table may be called &lt;code&gt;t_bd_customer&lt;/code&gt;. A field may carry a business-critical status code that only makes sense if you know the system behind it. A warehouse may split raw operational data, cleaned dimensions, and aggregated facts across schemas with names like &lt;code&gt;ods&lt;/code&gt;, &lt;code&gt;dw&lt;/code&gt;, and &lt;code&gt;staging&lt;/code&gt;. The schema is technically visible, but the meaning is not.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/chncaesar/db-semantic-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;db-semantic-mcp&lt;/code&gt;&lt;/a&gt;: a small MCP server that gives AI coding agents a safe semantic map of a database.&lt;/p&gt;

&lt;p&gt;It exposes table names, column types, comments, sample rows, and LLM-powered schema search. It supports PostgreSQL and SQL Server. It works with MCP-compatible agent clients such as OpenCode, Claude Code, Cursor, and similar tools.&lt;/p&gt;

&lt;p&gt;It deliberately does not execute SQL.&lt;/p&gt;

&lt;p&gt;That boundary is the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  The missing layer between agents and databases
&lt;/h2&gt;

&lt;p&gt;Most database integrations for agents start with query execution. Give the model a connection string, add a SQL tool, maybe add a read-only role, and let it ask the database questions.&lt;/p&gt;

&lt;p&gt;That can be useful. It is also a big first step.&lt;/p&gt;

&lt;p&gt;Before an agent writes or runs a query, it needs to answer more basic questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which tables are relevant?&lt;/li&gt;
&lt;li&gt;What do these column names mean?&lt;/li&gt;
&lt;li&gt;Which schema contains source data and which contains modeled data?&lt;/li&gt;
&lt;li&gt;Is this field a business status, a foreign key, a soft-delete marker, or an internal implementation detail?&lt;/li&gt;
&lt;li&gt;When a user says "inventory", "receivables", "customer", or "WIP", what tables should the agent inspect?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not SQL execution questions. They are database understanding questions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db-semantic-mcp&lt;/code&gt; focuses on that layer. It gives the agent enough structure to navigate the database without turning the database into a remote-control surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it exposes
&lt;/h2&gt;

&lt;p&gt;The server provides four MCP tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_tables&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List database tables with schema names and table comments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe_table&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect columns, types, nullability, and column comments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sample_data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetch a small number of example rows from a table.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_schema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search tables and columns semantically using an OpenAI-compatible LLM.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first three tools are direct metadata and sampling operations. They let an agent inspect the database the way a developer would: list tables, open one table, look at columns, check a few rows.&lt;/p&gt;

&lt;p&gt;The fourth tool is where the semantic layer matters.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;search_schema&lt;/code&gt; combines a cached schema snapshot with an optional Markdown file that describes your business terms, naming conventions, and database design decisions. The model can then resolve natural-language requests such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer receivables
WIP inventory
sales order
应收账款
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into the tables and columns that are likely to matter.&lt;/p&gt;

&lt;p&gt;This is especially useful for databases where the table names are technically consistent but not obvious to an agent. ERP databases, legacy SQL Server systems, and large warehouse schemas often fall into that category.&lt;/p&gt;




&lt;h2&gt;
  
  
  The semantic file is intentionally boring
&lt;/h2&gt;

&lt;p&gt;There is no new ontology format to learn. There is no vector database to deploy. There is no separate catalog service.&lt;/p&gt;

&lt;p&gt;You write a Markdown file.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Database Semantic Context&lt;/span&gt;

&lt;span class="gu"&gt;## Naming Conventions&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`ods.*`&lt;/span&gt; tables contain raw operational data.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`dw.*`&lt;/span&gt; tables contain modeled fact and dimension tables.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`staging.*`&lt;/span&gt; tables are temporary ETL staging tables.

&lt;span class="gu"&gt;## Business Terms&lt;/span&gt;

| Business term | Table(s) |
| --- | --- |
| Customer | ods.bd_customer, dw.dim_customer |
| Inventory | dw.fact_inventory_snapshot |
| WIP / work in progress | dw.fact_wip_by_lot |

&lt;span class="gu"&gt;## Design Decisions&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Monetary amounts are stored in integer cents.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`_modified_at`&lt;/span&gt; columns are incremental sync watermarks.
&lt;span class="p"&gt;-&lt;/span&gt; Soft deletes use &lt;span class="sb"&gt;`doc_status = 'D'`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file is loaded into the schema search prompt. It is the bridge between the database's physical structure and the vocabulary developers or business users actually use.&lt;/p&gt;

&lt;p&gt;The important design choice is that the semantic layer stays close to the team. It can live next to the project. It can be reviewed like documentation. It can be changed without re-indexing a vector store or migrating a metadata system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why no SQL execution?
&lt;/h2&gt;

&lt;p&gt;Because the first safe primitive an agent needs is not always a query tool.&lt;/p&gt;

&lt;p&gt;If an agent can execute arbitrary SQL, even read-only SQL, the safety problem becomes larger immediately. You need to think about permissions, row-level access, query cost, data exfiltration, audit logs, and prompt injection through data. Those problems are solvable, but they are not free.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db-semantic-mcp&lt;/code&gt; takes a narrower position: give the agent visibility into structure and meaning first.&lt;/p&gt;

&lt;p&gt;That makes the tool useful in more conservative environments. A team may be comfortable exposing table metadata, comments, and a few sample rows to an agent long before it is comfortable giving the agent a general SQL execution surface. The server still connects to the database, so it should be configured carefully, but its product boundary is intentionally smaller.&lt;/p&gt;

&lt;p&gt;The result is not a text-to-SQL platform. It is the layer before text-to-SQL. It helps the agent understand where it is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Built for real databases, not demo schemas
&lt;/h2&gt;

&lt;p&gt;The first implementation supported PostgreSQL. The current version also supports SQL Server through the same MCP interface.&lt;/p&gt;

&lt;p&gt;The backend is selected from the &lt;code&gt;DATABASE_URL&lt;/code&gt; scheme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://user:pass@localhost:5432/mydb
sqlserver://user:pass@host:1433?database=mydb&amp;amp;encrypt=disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That matters because a lot of valuable business data is not sitting in a neat Postgres app database. It is in SQL Server. It is in ERP systems. It is in databases with thousands of tables, inconsistent comments, historical naming conventions, and schemas that only a few people inside the company understand.&lt;/p&gt;

&lt;p&gt;For those databases, &lt;code&gt;db-semantic-mcp&lt;/code&gt; includes cache controls such as schema filters and table-prefix filters. If a SQL Server database contains thousands of tables but the useful business tables share prefixes like &lt;code&gt;t_pur_&lt;/code&gt;, &lt;code&gt;t_sal_&lt;/code&gt;, &lt;code&gt;t_stk_&lt;/code&gt;, or &lt;code&gt;t_bd_&lt;/code&gt;, the schema cache can focus on those areas.&lt;/p&gt;

&lt;p&gt;This is not about making a toy database easier to query. It is about making messy real databases navigable by an agent without pretending they are clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  How an agent uses it
&lt;/h2&gt;

&lt;p&gt;Once registered with an MCP client, the workflow is simple.&lt;/p&gt;

&lt;p&gt;An agent can start broad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;list_tables schema=dw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inspect a candidate table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;describe_table table=dw.fact_inventory_snapshot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then look at a few rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sample_data table=dw.fact_inventory_snapshot limit=3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or search semantically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_schema keyword="customer receivables"
search_schema keyword="应收账款"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent does not need to guess table names from memory. It does not need the user to paste schema dumps into every prompt. It can ask the database metadata server for the relevant context, then use that context in the coding task.&lt;/p&gt;

&lt;p&gt;For example, if the task is to modify an ETL pipeline, add a reporting endpoint, or debug a data mapping issue, the agent can first discover the database shape instead of hallucinating it.&lt;/p&gt;

&lt;p&gt;That is the value: better grounding before action.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuration shape
&lt;/h2&gt;

&lt;p&gt;The server is configured through environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
SEMANTIC_FILE=/path/to/SCHEMA.md
LLM_BASE_URL=https://api.openai.com/v1
LLM_API_KEY=sk-...
LLM_MODEL=gpt-4o-mini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LLM_API_KEY&lt;/code&gt; is only required for semantic search. The metadata tools work without it.&lt;/p&gt;

&lt;p&gt;An MCP client can register it as a local server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db-semantic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pg-semantic-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresql://user:pass@host:5432/dbname"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"SEMANTIC_FILE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/path/to/SCHEMA.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"LLM_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk-..."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command name still uses &lt;code&gt;pg-semantic-mcp&lt;/code&gt; for compatibility with the original PostgreSQL-only version. The package and repository now use the broader &lt;code&gt;db-semantic-mcp&lt;/code&gt; name because the server supports multiple backends.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this is good for
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;db-semantic-mcp&lt;/code&gt; is useful when an agent needs database context but should not start by executing SQL.&lt;/p&gt;

&lt;p&gt;Good fits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI coding agents working on backend services connected to PostgreSQL or SQL Server.&lt;/li&gt;
&lt;li&gt;Data platform projects where table names and business concepts are not obvious.&lt;/li&gt;
&lt;li&gt;ERP and legacy database exploration, especially when schemas are large.&lt;/li&gt;
&lt;li&gt;Teams that want agent-assisted development without immediately exposing query execution.&lt;/li&gt;
&lt;li&gt;Projects where a lightweight Markdown semantic layer is easier to maintain than a full data catalog.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not trying to replace a BI platform, a warehouse catalog, a governance product, or a complete text-to-SQL system.&lt;/p&gt;

&lt;p&gt;It is a small missing primitive: let the agent understand the database before it acts on the database.&lt;/p&gt;




&lt;h2&gt;
  
  
  The broader direction
&lt;/h2&gt;

&lt;p&gt;I think agent tooling is going to split into two categories.&lt;/p&gt;

&lt;p&gt;Some tools will make agents more powerful. They will let agents execute, mutate, deploy, administer, and automate more of the system.&lt;/p&gt;

&lt;p&gt;Other tools will make agents better grounded. They will expose state, constraints, readiness, history, metadata, and semantics in ways that reduce guessing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db-semantic-mcp&lt;/code&gt; belongs to the second category.&lt;/p&gt;

&lt;p&gt;It does not make the agent omnipotent. It gives the agent a map. In real engineering work, that is often the safer and more useful first step.&lt;/p&gt;

&lt;p&gt;Project: &lt;a href="https://github.com/chncaesar/db-semantic-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/chncaesar/db-semantic-mcp&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>mcp</category>
    </item>
    <item>
      <title>OpenCode Session Framework Internals</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Mon, 10 Aug 2026 08:22:40 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/opencode-session-framework-internals-1oj8</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/opencode-session-framework-internals-1oj8</guid>
      <description>&lt;p&gt;The easiest way to misunderstand an agent session is to treat it as a chat API call with a longer memory. Send a prompt, receive a response, append both to a transcript. That is enough for a demo. It is not enough for a coding agent.&lt;/p&gt;

&lt;p&gt;I ran into this while reading a small fleet client that drives remote OpenCode instances. The client does very little on the surface: create or reuse a session, send a prompt, wait for the remote agent to become idle, then fetch recent messages. The interesting part is that none of those verbs mean exactly what they mean in a normal request-response API. A prompt does not equal a response. A timeout does not mean failure. A session is not just a transcript. Status is not derived from the last line of text.&lt;/p&gt;

&lt;p&gt;That small client is a useful entry point because it exposes the shape of the real system. OpenCode's session design is not one function that calls a model. It is a framework for admitting work, serializing execution, projecting durable state, streaming observations, and letting clients recover when a long-running agent is still in flight.&lt;/p&gt;

&lt;p&gt;Primary code references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;opencode-fleet/src/tools.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opencode-fleet/src/session.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;opencode-fleet/src/node.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/server/routes/instance/httpapi/groups/session.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/session.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/prompt.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/run-state.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/status.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/processor.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/server/routes/instance/httpapi/handlers/event.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/input.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/run-coordinator.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/runner/llm.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/event.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/projector.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fleet client follows the desktop-compatible OpenCode API: &lt;code&gt;/session&lt;/code&gt;, &lt;code&gt;/session/:id/prompt_async&lt;/code&gt;, &lt;code&gt;/session/:id/message&lt;/code&gt;, &lt;code&gt;/session/status&lt;/code&gt;, and &lt;code&gt;/event&lt;/code&gt;. The newer V2/core API exposes the same architectural direction more explicitly through &lt;code&gt;/api/session&lt;/code&gt;, &lt;code&gt;/api/session/:id/prompt&lt;/code&gt;, &lt;code&gt;/api/session/active&lt;/code&gt;, &lt;code&gt;/api/session/:id/event&lt;/code&gt;, and the durable &lt;code&gt;SessionInput&lt;/code&gt; and &lt;code&gt;SessionEvent&lt;/code&gt; pipeline. Both matter because together they show the transition from a working client protocol to a cleaner internal runtime model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you are building an agent runtime, design sessions as execution containers, not as chat transcripts.&lt;/strong&gt; A real session framework has to preserve identity, admit work, run one continuation at a time, expose observable status, persist structured messages, and make interruption and recovery normal operations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Separate session identity from prompt execution.&lt;/strong&gt; A session can outlive any single prompt. It owns directory, project, agent, model, title, permissions, messages, parts, and runtime state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat prompt submission as admission.&lt;/strong&gt; The client should be able to submit work and return before the agent finishes. Completion is a separate observation problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serialize execution per session.&lt;/strong&gt; Multiple prompts may arrive while the agent is busy. The runtime needs a coordinator that runs at most one drain loop per session and coalesces follow-up work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expose status as runtime state.&lt;/strong&gt; Busy, idle, and retry are not reliable if inferred from text. They should come from the execution layer or from an authoritative active-session set.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use events as the observation boundary.&lt;/strong&gt; Clients should not poll messages to guess what happened. They should subscribe to session and message events, then keep a local projection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Persist messages as structured state.&lt;/strong&gt; Text is only one part. Tool calls, tool results, reasoning, files, snapshots, errors, and step boundaries need identity and lifecycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make timeout, interrupt, and reset distinct.&lt;/strong&gt; Timeout means the caller stopped waiting. Interrupt asks the runtime to stop work. Reset discards a client-side binding or context. These are different operations.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Minimal Client Contract
&lt;/h2&gt;

&lt;p&gt;The fleet client is intentionally small. It exposes MCP tools such as &lt;code&gt;fleet_create_session&lt;/code&gt;, &lt;code&gt;fleet_send_message&lt;/code&gt;, &lt;code&gt;fleet_get_session_status&lt;/code&gt;, &lt;code&gt;fleet_get_session_messages&lt;/code&gt;, &lt;code&gt;fleet_interrupt_session&lt;/code&gt;, and &lt;code&gt;fleet_reset_session&lt;/code&gt;. Behind those tools, there are only two main classes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SessionManager&lt;/code&gt; keeps an in-memory map from node name to active session ID. It lazily creates a session on first send, reuses the same session for future prompts, and recreates a session if the server returns &lt;code&gt;404&lt;/code&gt;. &lt;code&gt;OpenCodeNode&lt;/code&gt; wraps the remote HTTP API and owns a persistent SSE subscriber that listens to &lt;code&gt;/event&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important flow is short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fleet_send_message
-&amp;gt; SessionManager.send
-&amp;gt; get or create session
-&amp;gt; POST /session/:id/prompt_async
-&amp;gt; wait for session.status idle over SSE
-&amp;gt; GET /session/:id/message
-&amp;gt; extract assistant text or tool progress summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flow already contains several design choices worth copying.&lt;/p&gt;

&lt;p&gt;First, the client binds one long-lived session per remote node. It does not create a fresh session for every prompt. That preserves working context and makes follow-up prompts meaningful.&lt;/p&gt;

&lt;p&gt;Second, sending a prompt is asynchronous. &lt;code&gt;OpenCodeNode.sendPromptAsync(...)&lt;/code&gt; posts a user message to &lt;code&gt;/session/:id/prompt_async&lt;/code&gt; and returns after the server accepts it. The fleet client then waits for status separately. This is the right split. If the same request both submits work and waits for the entire agent loop to finish, the client has no clean way to distinguish "the server accepted my work but the agent is still running" from "the server never accepted my work."&lt;/p&gt;

&lt;p&gt;Third, timeout is not treated as failure. &lt;code&gt;SessionManager.send(...)&lt;/code&gt; catches &lt;code&gt;TimeoutError&lt;/code&gt;, fetches partial messages, marks &lt;code&gt;timedOut: true&lt;/code&gt;, and tells the caller the remote agent is likely still running. That is exactly the behavior an agent coordinator needs. In a coding-agent runtime, a slow task is often useful work, not a broken request.&lt;/p&gt;

&lt;p&gt;Fourth, reset is guarded. &lt;code&gt;fleet_reset_session&lt;/code&gt; checks status and refuses to reset a busy session. This is not just user-interface caution. Resetting while a remote agent is running loses the caller's handle to in-flight work. The agent may still write files, ask for permissions, or finish with output after the caller has thrown away the session ID. A framework should make that hard to do accidentally.&lt;/p&gt;

&lt;p&gt;The fleet implementation is not the whole OpenCode session architecture. It is a client-side adaptation. But it shows what the server must provide: stable session IDs, async prompt admission, observable status, message history, interruption, and enough structured message parts to explain progress before final text exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Creation Is Identity, Not Execution
&lt;/h2&gt;

&lt;p&gt;In the desktop-compatible API, the legacy route group defines &lt;code&gt;POST /session&lt;/code&gt; as &lt;code&gt;session.create&lt;/code&gt;. The handler eventually calls &lt;code&gt;Session.create(...)&lt;/code&gt;, which creates a session record with an ID, slug, project, directory, path, optional workspace, title, agent, model, permissions, token counters, and timestamps. It publishes &lt;code&gt;session.created&lt;/code&gt; through the event bridge. Projectors then write that session into SQLite.&lt;/p&gt;

&lt;p&gt;Nothing has run yet.&lt;/p&gt;

&lt;p&gt;That distinction is easy to miss. A session is not "the model is working." A session is the durable container in which work may later happen. It represents a place in the filesystem, a selected agent and model, permission context, and a message history boundary.&lt;/p&gt;

&lt;p&gt;The newer V2/core path makes the same idea clearer. &lt;code&gt;SessionV2.Service.create(...)&lt;/code&gt; resolves the project for a location, creates a &lt;code&gt;Session.Info&lt;/code&gt;, publishes a created event, and returns the stored session. Execution is not part of creation. The session starts idle.&lt;/p&gt;

&lt;p&gt;This matters for agent framework design because the session ID becomes the join key for everything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user prompts&lt;/li&gt;
&lt;li&gt;assistant messages&lt;/li&gt;
&lt;li&gt;tool calls&lt;/li&gt;
&lt;li&gt;permission requests&lt;/li&gt;
&lt;li&gt;question requests&lt;/li&gt;
&lt;li&gt;status events&lt;/li&gt;
&lt;li&gt;snapshots and diffs&lt;/li&gt;
&lt;li&gt;model and agent switches&lt;/li&gt;
&lt;li&gt;compaction checkpoints&lt;/li&gt;
&lt;li&gt;interrupt and reset operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If session creation also starts execution, that boundary gets muddy. If a session is only a transcript row, it will not have enough identity to support tooling, permissions, status, or recovery. A good session object should answer: where is this agent working, what policy applies, what model and agent should subsequent turns use, and which durable history does this execution belong to?&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt Submission Is Admission
&lt;/h2&gt;

&lt;p&gt;The desktop-compatible async endpoint is &lt;code&gt;/session/:sessionID/prompt_async&lt;/code&gt;. Its handler requires the session, then forks &lt;code&gt;promptSvc.prompt(...)&lt;/code&gt; into the server scope and immediately returns &lt;code&gt;204 No Content&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means the HTTP response does not mean "the assistant finished." It means "the server accepted responsibility for starting the prompt work." The actual work continues in a fiber.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;SessionPrompt.prompt(...)&lt;/code&gt;, OpenCode creates a user message, stores its parts, touches the session, applies any per-prompt tool permission overrides, and then calls &lt;code&gt;loop(...)&lt;/code&gt; unless the prompt was marked &lt;code&gt;noReply&lt;/code&gt;. The loop is the real execution path.&lt;/p&gt;

&lt;p&gt;The V2/core API names the same boundary more explicitly. &lt;code&gt;POST /api/session/:sessionID/prompt&lt;/code&gt; calls &lt;code&gt;SessionV2.Service.prompt(...)&lt;/code&gt;. That service verifies the session, resolves the prompt, chooses a message ID, and calls &lt;code&gt;SessionInput.admit(...)&lt;/code&gt;. Admission publishes &lt;code&gt;session.next.prompt.admitted&lt;/code&gt; as a durable event. Only after the input is durably admitted does the service call &lt;code&gt;execution.wake(sessionID)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the key design move: prompt submission becomes durable input admission plus execution wakeup.&lt;/p&gt;

&lt;p&gt;That gives the runtime several properties that a direct "call the model now" design does not have.&lt;/p&gt;

&lt;p&gt;The prompt has an identity before the model runs. The system can reject duplicate message IDs. It can record that a prompt entered the session even if execution starts slightly later. It can choose not to resume immediately. It can queue or steer inputs. It can replay durable input history into a projected message stream. It can recover from client disconnects because the prompt is not merely an in-memory function argument.&lt;/p&gt;

&lt;p&gt;If you are building an agent runtime, this is one of the most important principles to copy. Do not make the user's prompt disappear into a model call. Admit it into the session first. Then schedule execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Execution Needs A Per-Session Coordinator
&lt;/h2&gt;

&lt;p&gt;Once prompts can be admitted independently from execution, the runtime needs a rule for what happens when work arrives while the session is already busy.&lt;/p&gt;

&lt;p&gt;OpenCode has two implementations that reveal the same idea.&lt;/p&gt;

&lt;p&gt;In the desktop-compatible path, &lt;code&gt;SessionRunState&lt;/code&gt; keeps a per-session &lt;code&gt;Runner&lt;/code&gt;. The runner has states such as &lt;code&gt;Idle&lt;/code&gt;, &lt;code&gt;Running&lt;/code&gt;, &lt;code&gt;Shell&lt;/code&gt;, and &lt;code&gt;ShellThenRun&lt;/code&gt;. &lt;code&gt;ensureRunning(...)&lt;/code&gt; starts work if idle. If a run is already active, it waits for that active run instead of starting a second one. If shell work is active, it can queue a run after the shell finishes. &lt;code&gt;cancel(...)&lt;/code&gt; interrupts the current fiber and returns the runner to idle.&lt;/p&gt;

&lt;p&gt;In V2/core, &lt;code&gt;SessionRunCoordinator&lt;/code&gt; is smaller and more explicit. It maintains a map from session ID to active entry. &lt;code&gt;wake(sessionID)&lt;/code&gt; starts a drain fiber if idle. If a fiber is already running, it sets &lt;code&gt;pendingWake = true&lt;/code&gt;. When the active fiber settles successfully, the coordinator starts a successor if a wake was recorded. &lt;code&gt;interrupt(sessionID)&lt;/code&gt; marks the entry as stopping, clears pending wake, and interrupts the owner fiber.&lt;/p&gt;

&lt;p&gt;That gives OpenCode an important invariant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;one session -&amp;gt; at most one active drain loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different sessions can run concurrently. The same session cannot accidentally run two provider turns against the same history at the same time.&lt;/p&gt;

&lt;p&gt;This is not an implementation detail. It is the difference between a predictable agent session and a race condition factory. Without a per-session coordinator, two prompts can read the same context, both call the model, both write assistant messages, and both execute tools against the filesystem. In a coding agent, that is dangerous. The second prompt may assume files are unchanged while the first prompt is editing them. Tool permissions and status become ambiguous. The UI cannot honestly say what the session is doing.&lt;/p&gt;

&lt;p&gt;The right abstraction is not a mutex around the HTTP handler. It is a session execution coordinator. It should live at the runtime layer, below all clients, so desktop, TUI, MCP clients, scripts, and external tools all obey the same rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Runner Is A Drain Loop, Not One Model Call
&lt;/h2&gt;

&lt;p&gt;The execution loop itself is also larger than one model call.&lt;/p&gt;

&lt;p&gt;In the desktop-compatible path, &lt;code&gt;SessionPrompt.runLoop(...)&lt;/code&gt; repeatedly sets the session busy, loads compacted history, finds the latest user and assistant state, handles subtasks and compaction tasks, resolves the current agent and model, builds tools, assembles system instructions, converts stored messages into provider messages, and calls &lt;code&gt;SessionProcessor.process(...)&lt;/code&gt;. The processor consumes the provider stream and updates message parts as text, reasoning, tool calls, tool results, errors, and finish state arrive. If the model asked for tools, the loop continues so the tool results can be sent back to the model.&lt;/p&gt;

&lt;p&gt;In V2/core, &lt;code&gt;SessionRunner.run(...)&lt;/code&gt; follows the same conceptual shape. It checks pending steer or queue inputs. &lt;code&gt;runTurnAttempt(...)&lt;/code&gt; promotes pending input into active context, prepares system context, resolves model and tools, builds an &lt;code&gt;LLM.request(...)&lt;/code&gt;, streams provider events, publishes structured session events, settles local tools, and continues if tool calls or new steering require another turn.&lt;/p&gt;

&lt;p&gt;The naming matters. A good agent runtime does not have a &lt;code&gt;completeChat(...)&lt;/code&gt; function. It has a drain loop. The loop drains admitted work until the session reaches a stable idle boundary.&lt;/p&gt;

&lt;p&gt;That loop has to deal with continuation conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the model requested tools&lt;/li&gt;
&lt;li&gt;tools finished and their results need to be sent back&lt;/li&gt;
&lt;li&gt;new steering arrived while a turn was active&lt;/li&gt;
&lt;li&gt;queued input is waiting&lt;/li&gt;
&lt;li&gt;compaction is required before another provider call&lt;/li&gt;
&lt;li&gt;the provider failed before durable assistant output existed&lt;/li&gt;
&lt;li&gt;the user denied permission and the loop should stop&lt;/li&gt;
&lt;li&gt;the session was interrupted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those conditions are bolted onto a single request handler, the handler becomes impossible to reason about. OpenCode keeps them inside session execution. Clients submit work, observe events, and interrupt if needed. They do not own the agent loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status Should Come From Execution, Not Transcript Guessing
&lt;/h2&gt;

&lt;p&gt;The fleet client originally has a tempting fallback: inspect messages and infer busy or idle by looking for step-finish parts after the last user message. That kind of fallback is useful for compatibility, but it should not be the primary status model.&lt;/p&gt;

&lt;p&gt;OpenCode's desktop-compatible runtime has &lt;code&gt;SessionStatus&lt;/code&gt;. It keeps an instance-local map of non-idle sessions. &lt;code&gt;set(sessionID, { type: "busy" })&lt;/code&gt; publishes a &lt;code&gt;session.status&lt;/code&gt; event and stores the status. &lt;code&gt;set(sessionID, { type: "idle" })&lt;/code&gt; publishes both &lt;code&gt;session.status&lt;/code&gt; and deprecated &lt;code&gt;session.idle&lt;/code&gt;, then deletes the session from the map. A missing status means idle.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SessionRunState&lt;/code&gt; calls &lt;code&gt;status.set(...busy...)&lt;/code&gt; when a runner becomes active and &lt;code&gt;status.set(...idle...)&lt;/code&gt; when the runner returns to idle. &lt;code&gt;SessionProcessor&lt;/code&gt; sets busy while processing provider streams and sets retry status during retry backoff. The server exposes the status map through &lt;code&gt;GET /session/status&lt;/code&gt;, and it also streams &lt;code&gt;session.status&lt;/code&gt; events through &lt;code&gt;/event&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The V2/core API exposes the same concept as &lt;code&gt;GET /api/session/active&lt;/code&gt;. It returns the set of foreground drains currently owned by this OpenCode process. If a session appears there, it is running. If it is absent, it is inactive.&lt;/p&gt;

&lt;p&gt;The lesson is simple: status should come from the execution owner.&lt;/p&gt;

&lt;p&gt;Message history is a projection of what happened. It is not the authority for what is currently happening. A session may be busy before the first assistant step appears. A provider may be retrying without writing new visible text. A tool may be running with no final assistant answer yet. A streamed text delta may arrive before the durable final text part. If a client has to scrape messages to infer status, the runtime has failed to expose a basic operational fact.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;opencode-fleet&lt;/code&gt; keeps a persistent SSE status stream and optimistically marks a session busy immediately after &lt;code&gt;prompt_async&lt;/code&gt; returns. There is a race window between prompt admission and the first SSE event. A client that immediately checks status should not conclude "idle" just because the event has not arrived yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Events Are The Observation Boundary
&lt;/h2&gt;

&lt;p&gt;OpenCode clients do not need to keep asking, "what changed?" They subscribe.&lt;/p&gt;

&lt;p&gt;The desktop-compatible &lt;code&gt;/event&lt;/code&gt; endpoint registers an eager listener against &lt;code&gt;EventV2Bridge&lt;/code&gt;, filters events by instance directory and workspace, emits a synthetic &lt;code&gt;server.connected&lt;/code&gt;, sends heartbeat events, and streams JSON payloads as SSE. The global event endpoint wraps the &lt;code&gt;GlobalBus&lt;/code&gt; and carries cross-instance events. The newer server package exposes &lt;code&gt;/api/event&lt;/code&gt; for all server events and &lt;code&gt;/api/session/:sessionID/event&lt;/code&gt; for durable per-session events.&lt;/p&gt;

&lt;p&gt;That gives the desktop app and external clients a common observation model. The app's &lt;code&gt;server-session.ts&lt;/code&gt; applies events into a local Solid store. It updates session info on &lt;code&gt;session.created&lt;/code&gt; and &lt;code&gt;session.updated&lt;/code&gt;, status on &lt;code&gt;session.status&lt;/code&gt;, messages on &lt;code&gt;message.updated&lt;/code&gt;, parts on &lt;code&gt;message.part.updated&lt;/code&gt;, deltas on &lt;code&gt;message.part.delta&lt;/code&gt;, permissions on &lt;code&gt;permission.asked&lt;/code&gt;, questions on &lt;code&gt;question.asked&lt;/code&gt;, and so on. It also reconciles optimistic local messages with confirmed server events.&lt;/p&gt;

&lt;p&gt;This local projection is not just for UI polish. It is a fundamental architecture choice. The server owns truth. Clients maintain projections.&lt;/p&gt;

&lt;p&gt;That separation solves several problems.&lt;/p&gt;

&lt;p&gt;It lets a client show progress before a final response exists. It lets a client reconnect and refresh from durable history when needed. It keeps streaming deltas separate from final durable values. It lets different clients observe the same session without embedding execution logic in each client. It gives external tools a debugging path: subscribe to events, then inspect messages and parts when something looks wrong.&lt;/p&gt;

&lt;p&gt;If you build an agent runtime without an event boundary, every client becomes a partial runtime. The UI will poll messages. The CLI will invent a different status heuristic. External tools will guess when work is done. Eventually those guesses disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Messages Are Structured Projections
&lt;/h2&gt;

&lt;p&gt;The most visible artifact of a session is the conversation. But OpenCode does not treat the conversation as plain text.&lt;/p&gt;

&lt;p&gt;In the desktop-compatible projection, session messages live as message rows and part rows. A user message can contain text, files, agents, and subtasks. An assistant message can contain text, reasoning, tool parts, step markers, snapshots, patches, retries, compaction parts, and errors. A tool part has a call ID, tool name, input, status, output, metadata, attachments, and timing.&lt;/p&gt;

&lt;p&gt;That shape is why &lt;code&gt;opencode-fleet&lt;/code&gt; can return useful partial output when a prompt times out. If the assistant has no text yet but tool calls are running, the client can summarize tool activity instead of returning an empty string. It can say the agent is busy and list the tools in progress.&lt;/p&gt;

&lt;p&gt;V2/core pushes this further with durable session events and projected messages. &lt;code&gt;SessionInput.admit(...)&lt;/code&gt; records prompt admission. &lt;code&gt;SessionInput.promoteSteers(...)&lt;/code&gt; publishes &lt;code&gt;session.next.prompted&lt;/code&gt;. &lt;code&gt;createLLMEventPublisher(...)&lt;/code&gt; converts provider events into session events such as &lt;code&gt;session.next.step.started&lt;/code&gt;, &lt;code&gt;session.next.text.delta&lt;/code&gt;, &lt;code&gt;session.next.text.ended&lt;/code&gt;, &lt;code&gt;session.next.tool.called&lt;/code&gt;, &lt;code&gt;session.next.tool.success&lt;/code&gt;, &lt;code&gt;session.next.tool.failed&lt;/code&gt;, and &lt;code&gt;session.next.step.ended&lt;/code&gt;. &lt;code&gt;SessionProjector&lt;/code&gt; turns those events into queryable message rows.&lt;/p&gt;

&lt;p&gt;This creates three useful layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Durable events&lt;/td&gt;
&lt;td&gt;What happened, in order, with session sequence numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Projected messages&lt;/td&gt;
&lt;td&gt;Query-friendly session state for UI and clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client store&lt;/td&gt;
&lt;td&gt;Local observable cache, including optimistic and streaming state&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That layering is more work than appending text to an array. It is also what makes a coding-agent session debuggable. If a tool failed, you can find the tool call. If a provider streamed text and then failed, you can represent partial output and final error separately. If a permission request blocked execution, it has identity. If compaction changed the context boundary, it is a session event and a message part, not an invisible truncation.&lt;/p&gt;

&lt;p&gt;The design principle is that the transcript is a projection, not the source of truth. The source of truth is the session's structured event and message state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interrupt Is Not Reset
&lt;/h2&gt;

&lt;p&gt;Long-running agents need lifecycle controls. OpenCode exposes several, and the differences matter.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prompt_async&lt;/code&gt; starts work and returns immediately. &lt;code&gt;session.status&lt;/code&gt; or &lt;code&gt;/api/session/active&lt;/code&gt; tells a client whether work is still running. &lt;code&gt;/session/:id/abort&lt;/code&gt; or &lt;code&gt;/api/session/:id/interrupt&lt;/code&gt; asks the runtime to stop active execution. Fetching messages shows what has happened so far. Resetting a client binding merely means the client stops using that session ID for future sends.&lt;/p&gt;

&lt;p&gt;These operations should not be collapsed.&lt;/p&gt;

&lt;p&gt;The fleet client handles this well. On timeout, it does not reset. It tells the caller the agent is still running and recommends checking status, inspecting messages, waiting, or interrupting. &lt;code&gt;fleet_interrupt_session&lt;/code&gt; sends an abort signal but does not delete the session or clear the binding. &lt;code&gt;fleet_reset_session&lt;/code&gt; discards the cached session ID only after checking that the session is not busy.&lt;/p&gt;

&lt;p&gt;That behavior reflects the server-side reality. In the desktop-compatible runtime, &lt;code&gt;SessionRunState.cancel(...)&lt;/code&gt; interrupts active fibers and cancels related background jobs. The runner transitions back to idle and status events are emitted. In V2/core, &lt;code&gt;SessionRunCoordinator.interrupt(...)&lt;/code&gt; marks the active entry as stopping, clears pending wake, and interrupts the owner fiber. The runner then settles interrupted tools and assistant state.&lt;/p&gt;

&lt;p&gt;A reset cannot do that. Reset is a client-side context decision. Interrupt is an execution decision. Delete is a storage decision. Timeout is a waiting decision. If your framework uses one "cancel" or "reset" button for all four, users will eventually lose work or leave orphaned execution behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compatibility Is A Shell Around The Runtime
&lt;/h2&gt;

&lt;p&gt;One subtle part of OpenCode's current codebase is that it has both the desktop-compatible instance API and the newer V2/core API mounted in the same process. The route tree in &lt;code&gt;packages/opencode/src/server/routes/instance/httpapi/server.ts&lt;/code&gt; provides legacy routes such as &lt;code&gt;/session/:id/prompt_async&lt;/code&gt; and &lt;code&gt;/event&lt;/code&gt;, while also mounting the newer &lt;code&gt;@opencode-ai/server&lt;/code&gt; handlers for &lt;code&gt;/api/session&lt;/code&gt;, &lt;code&gt;/api/event&lt;/code&gt;, and related endpoints.&lt;/p&gt;

&lt;p&gt;That can look confusing if you read only endpoint names. It makes more sense if you separate protocol compatibility from runtime architecture.&lt;/p&gt;

&lt;p&gt;The legacy API exists because clients depend on it. The desktop UI, generated SDKs, compatibility wrappers, CLI paths, and external tools still speak that language. It has concepts such as &lt;code&gt;promptAsync&lt;/code&gt;, &lt;code&gt;message.part.delta&lt;/code&gt;, and &lt;code&gt;session.status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The V2/core architecture makes the internal model more explicit. Prompt admission is a durable event. Pending inputs live in &lt;code&gt;SessionInputTable&lt;/code&gt;. Execution is coordinated through &lt;code&gt;SessionExecution&lt;/code&gt; and &lt;code&gt;SessionRunCoordinator&lt;/code&gt;. Session events can be replayed per aggregate. Projectors build structured message rows from durable events.&lt;/p&gt;

&lt;p&gt;The lesson for agent-runtime builders is not "copy these exact endpoints." The lesson is to keep the compatibility shell thin. Let old clients keep their contract, but move the runtime toward clearer boundaries: admission, execution, events, projection, and observation.&lt;/p&gt;

&lt;p&gt;If compatibility code owns the runtime model, every old endpoint shape becomes a permanent architectural constraint. If the runtime owns the model, compatibility handlers can translate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What To Copy
&lt;/h2&gt;

&lt;p&gt;If I were designing a session framework for a new coding agent, I would copy these pieces first.&lt;/p&gt;

&lt;p&gt;Create sessions independently from prompts. A session should be a durable execution container with location, agent, model, permissions, title, timestamps, and identity. It should be useful before anything is running.&lt;/p&gt;

&lt;p&gt;Admit prompts before running them. Give each prompt or user message an ID. Persist it or publish it durably. Only then wake execution. That makes retries, duplicate detection, queueing, and recovery possible.&lt;/p&gt;

&lt;p&gt;Run one drain loop per session. Do not let every HTTP request or client call start its own model execution. A coordinator should own session execution and serialize work for that session while allowing other sessions to run concurrently.&lt;/p&gt;

&lt;p&gt;Make status authoritative. Either expose a status map or an active execution set. Busy, idle, and retry should be runtime facts, not message-history guesses.&lt;/p&gt;

&lt;p&gt;Stream events. Clients should subscribe to server events and maintain projections. Polling can exist as a fallback, but it should not be the core observation model.&lt;/p&gt;

&lt;p&gt;Persist structured message parts. Text alone is not enough. Tool calls, tool results, reasoning, files, errors, snapshots, and step boundaries need their own identities and states.&lt;/p&gt;

&lt;p&gt;Design lifecycle controls separately. Timeout, wait, interrupt, reset, delete, and fork are not the same operation. Give them separate APIs and make dangerous transitions explicit.&lt;/p&gt;

&lt;p&gt;Keep compatibility outside the core. Endpoint names will change. SDK shapes will change. Desktop and CLI needs will differ. The runtime should be stable underneath those clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Shape
&lt;/h2&gt;

&lt;p&gt;The simplest useful mental model for an agent session is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session identity
  -&amp;gt; admitted inputs
  -&amp;gt; per-session execution coordinator
  -&amp;gt; agent drain loop
  -&amp;gt; structured events
  -&amp;gt; projected messages
  -&amp;gt; client-side observable state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shape is more complicated than a chat completion wrapper. But the complexity is paying for real product requirements: long-running work, tool execution, concurrent clients, interruption, retries, partial output, permissions, compaction, and debugging.&lt;/p&gt;

&lt;p&gt;The mistake is to start with the provider API and build upward. Provider APIs know how to produce tokens and tool-call requests. They do not know what a session means in your product. They do not know how to serialize work per project directory. They do not know when a client timed out but the agent is still running. They do not know how your UI should reconcile optimistic messages with durable events. They do not know what it means to reset a remote worker safely.&lt;/p&gt;

&lt;p&gt;The session framework owns those answers.&lt;/p&gt;

&lt;p&gt;OpenCode's implementation is valuable because it exposes that boundary. The model call is inside the session runtime, not the other way around. Prompts are admitted before execution. Execution is coordinated per session. Status is published by the runner. Events are the observation surface. Messages are structured projections. Clients can be thin because the runtime has a real shape.&lt;/p&gt;

&lt;p&gt;That is the design principle worth taking: build the session as the agent's operating context. The chat transcript is only one view of it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading. I build tools for AI coding agents at &lt;a href="https://github.com/chncaesar" rel="noopener noreferrer"&gt;github.com/chncaesar&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-db-clean" rel="noopener noreferrer"&gt;opencode-db-clean&lt;/a&gt; — reclaim GBs of SQLite disk space&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-waitfor" rel="noopener noreferrer"&gt;opencode-waitfor&lt;/a&gt; — proper readiness checks, no more sleep loops&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-session-reflection" rel="noopener noreferrer"&gt;opencode-session-reflection&lt;/a&gt; — turn past sessions into workflow improvements&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-fleet" rel="noopener noreferrer"&gt;opencode-fleet&lt;/a&gt; — multi-node OpenCode orchestration&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opencode</category>
    </item>
    <item>
      <title>My OpenCode Database Was Mostly Empty Space</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Wed, 22 Jul 2026 07:47:16 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/my-opencode-database-was-mostly-empty-space-1c49</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/my-opencode-database-was-mostly-empty-space-1c49</guid>
      <description>&lt;p&gt;OpenCode keeps a lot of useful history.&lt;/p&gt;

&lt;p&gt;That is one of the reasons I like it. A coding-agent session is not just a chat transcript. It is a record of what I asked, what the agent did, which tools ran, what failed, what got fixed, and what context existed at the time. I have argued before that the agent session is becoming a log file. I still believe that.&lt;/p&gt;

&lt;p&gt;Then I noticed the log file was getting large.&lt;/p&gt;

&lt;p&gt;On one machine, &lt;code&gt;~/.local/share/opencode/opencode.db&lt;/code&gt; had grown past a gigabyte. That was not shocking by itself. I use OpenCode heavily. Long sessions produce many messages, tool results, shell outputs, file reads, and state transitions. A local database growing over time is expected.&lt;/p&gt;

&lt;p&gt;What was surprising was what SQLite reported after looking inside the file: much of it was not live data anymore.&lt;/p&gt;

&lt;p&gt;It was empty space.&lt;/p&gt;




&lt;h2&gt;
  
  
  The misleading part of deleting data
&lt;/h2&gt;

&lt;p&gt;Most developers learn an intuitive model of storage that is only partly true.&lt;/p&gt;

&lt;p&gt;If I delete rows from a database, I expect the database file to get smaller. If I archive old sessions, compact history, or remove records, I expect disk usage to fall. At the application level, that feels right: fewer records should mean fewer bytes.&lt;/p&gt;

&lt;p&gt;SQLite does not normally work that way.&lt;/p&gt;

&lt;p&gt;When rows are deleted, SQLite can reuse the freed pages for future writes, but it does not necessarily return those pages to the filesystem. The file can stay the same size while containing a growing internal pool of reusable pages. That pool is the freelist.&lt;/p&gt;

&lt;p&gt;The important distinction is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database file size != live data size
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file can be 1.3 GB while the live data is only 525 MB. The remaining 766 MB can be pages SQLite is keeping around for reuse. From SQLite's perspective, that space is available. From the filesystem's perspective, it is still occupied.&lt;/p&gt;

&lt;p&gt;That is not corruption. It is not necessarily a bug in the data model. It is how SQLite behaves unless the database is configured and vacuumed in a way that returns free pages to the OS.&lt;/p&gt;

&lt;p&gt;For a normal application database, this may not matter. For a local agent runtime that stores every tool-heavy session in one file, it becomes noticeable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why OpenCode makes this visible
&lt;/h2&gt;

&lt;p&gt;Agent sessions are unusually good at producing large, bursty storage.&lt;/p&gt;

&lt;p&gt;A normal chat app stores messages. A coding agent stores messages plus tool calls, tool results, shell output, file contents, structured parts, todos, events, and session metadata. A single task can contain a surprising amount of durable state.&lt;/p&gt;

&lt;p&gt;OpenCode also has session compaction. Compaction is useful for model context management, but it is easy to confuse three different layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What changes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model-visible context&lt;/td&gt;
&lt;td&gt;Older conversation can be summarized behind a checkpoint.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durable history&lt;/td&gt;
&lt;td&gt;Original rows can still exist in the database.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite file layout&lt;/td&gt;
&lt;td&gt;Deleted or obsolete pages may remain inside the file as freelist space.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compaction helps the model continue when context gets too large. It does not mean the database file shrinks. Even when rows are removed by some maintenance path, SQLite may keep the freed pages in the same file.&lt;/p&gt;

&lt;p&gt;This is the kind of problem that is easy to misdiagnose if you only look at &lt;code&gt;du -h opencode.db&lt;/code&gt;. A large file does not tell you how much live data exists. It only tells you how much disk the file currently occupies.&lt;/p&gt;

&lt;p&gt;The first tool I wanted was not a cleaner. It was a measurement.&lt;/p&gt;




&lt;h2&gt;
  
  
  The useful number is freelist percentage
&lt;/h2&gt;

&lt;p&gt;SQLite already exposes the numbers needed to understand the problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;page_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;freelist_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From those values, the diagnosis is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;freelist_bytes = freelist_count * page_size
live_bytes = (page_count - freelist_count) * page_size
freelist_pct = freelist_count / page_count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives a more useful report than raw file size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OpenCode Database Health Report
----------------------------------------------------------
  File size:     1.3 GB
  Page size:     4.0 KB
  Journal mode:  wal
  Auto-vacuum:   OFF (NONE)

Storage
----------------------------------------------------------
  Live data:     525 MB
  Freelist:      766 MB  (56% of file)
  -&amp;gt; VACUUM would reclaim ~766 MB
  -&amp;gt; Estimated result:  ~525 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes the conversation. The question is no longer "why is the database 1.3 GB?" The better question is "how much of this file is still live data?"&lt;/p&gt;

&lt;p&gt;If the freelist is small, there is nothing urgent to do. If half the file is freelist pages, a cleanup can reclaim real disk space.&lt;/p&gt;

&lt;p&gt;That became &lt;a href="https://github.com/chncaesar/opencode-db-clean" rel="noopener noreferrer"&gt;&lt;code&gt;ocdbc&lt;/code&gt;&lt;/a&gt;: OpenCode Database Cleaner.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I did not want a clever cleaner
&lt;/h2&gt;

&lt;p&gt;The dangerous version of this tool would delete sessions.&lt;/p&gt;

&lt;p&gt;That is not what I wanted. I did not want a policy engine deciding which conversations were old, stale, low-value, or safe to remove. Agent sessions are evidence. They contain exactly the kind of messy operational detail I often need later: commands, errors, constraints, design decisions, failed attempts, and verification output.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;ocdbc&lt;/code&gt; has a narrower job.&lt;/p&gt;

&lt;p&gt;It does two things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ocdbc analyze
ocdbc vacuum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;analyze&lt;/code&gt; is read-only. It reports database size, live data, freelist space, table sizes, session age distribution, and the largest messages.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vacuum&lt;/code&gt; does not decide what history should exist. It asks SQLite to rebuild the database file so unused pages can be returned to the filesystem. The goal is physical cleanup, not semantic deletion.&lt;/p&gt;

&lt;p&gt;That boundary matters. A tool that deletes history needs product policy. A tool that reports freelist space and runs a safe SQLite maintenance sequence can stay much smaller.&lt;/p&gt;




&lt;h2&gt;
  
  
  The safety sequence matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;VACUUM&lt;/code&gt; sounds boring until you remember what file it is operating on.&lt;/p&gt;

&lt;p&gt;This is the local database for an agent runtime. If OpenCode is running while the database is being rebuilt, I do not want to discover edge cases by corrupting my own session history. If the write-ahead log has committed data that has not been checkpointed into the main database file, I do not want to back up an incomplete view. If the database is already corrupt, I do not want the cleanup tool to make the failure harder to reason about.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;ocdbc vacuum&lt;/code&gt; is intentionally conservative.&lt;/p&gt;

&lt;p&gt;The sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Refuse to run if &lt;code&gt;fuser&lt;/code&gt; shows OpenCode still has the database open.&lt;/li&gt;
&lt;li&gt;Checkpoint the WAL before copying the database.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;PRAGMA integrity_check&lt;/code&gt; before making changes.&lt;/li&gt;
&lt;li&gt;Create a timestamped backup.&lt;/li&gt;
&lt;li&gt;Open the backup and verify its integrity too.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;auto_vacuum = INCREMENTAL&lt;/code&gt; so future free pages can be reclaimed incrementally.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;VACUUM&lt;/code&gt; to rebuild the file.&lt;/li&gt;
&lt;li&gt;Re-enable WAL, because &lt;code&gt;VACUUM&lt;/code&gt; can reset journal mode.&lt;/li&gt;
&lt;li&gt;Run a final integrity check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The order is the point.&lt;/p&gt;

&lt;p&gt;Checkpoint before backup means the &lt;code&gt;.db&lt;/code&gt; file contains committed WAL data before it is copied. Verifying the backup means the fallback is not just a file that happened to exist. Refusing to run while another process has the database open prevents a maintenance tool from racing the application it is trying to help.&lt;/p&gt;

&lt;p&gt;There are override flags, but they are explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ocdbc vacuum &lt;span class="nt"&gt;--force&lt;/span&gt;
ocdbc vacuum &lt;span class="nt"&gt;--skip-fuser&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
ocdbc vacuum &lt;span class="nt"&gt;--no-backup&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default path optimizes for not losing data.&lt;/p&gt;




&lt;h2&gt;
  
  
  A small tool is enough
&lt;/h2&gt;

&lt;p&gt;The whole package is a single Python module with no runtime dependencies. Installation is intentionally uninteresting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;ocdbc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ocdbc analyze
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the report shows significant freelist bloat, close OpenCode and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ocdbc vacuum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it.&lt;/p&gt;

&lt;p&gt;The tool does not need a daemon. It does not need to understand model context. It does not need to parse sessions or judge which messages matter. It only needs to expose the maintenance operation I wanted OpenCode users to have available when the database file becomes misleadingly large.&lt;/p&gt;

&lt;p&gt;That is the pattern I keep coming back to with agent tooling. The best tool is often not the most intelligent one. It is the one that gives the agent or developer a missing primitive with a tight safety boundary.&lt;/p&gt;

&lt;p&gt;For readiness checks, that primitive was &lt;code&gt;wait_for&lt;/code&gt;: poll a URL, port, or command until the condition is actually true.&lt;/p&gt;

&lt;p&gt;For session review, that primitive was &lt;code&gt;session_reflection&lt;/code&gt;: treat recent sessions as evidence instead of disposable scrollback.&lt;/p&gt;

&lt;p&gt;For OpenCode database bloat, the primitive is &lt;code&gt;ocdbc&lt;/code&gt;: show the difference between live data and empty pages, then run the boring SQLite cleanup correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The broader lesson
&lt;/h2&gt;

&lt;p&gt;AI-assisted development creates new kinds of local infrastructure.&lt;/p&gt;

&lt;p&gt;The agent runtime is not just a prompt window. It has tools, permissions, sessions, transcripts, databases, plugins, logs, background state, and failure modes. Once that runtime becomes part of daily work, it needs the same boring maintenance primitives every other developer toolchain needs.&lt;/p&gt;

&lt;p&gt;Sometimes the fix is not a better model.&lt;/p&gt;

&lt;p&gt;Sometimes it is knowing that your 1.3 GB database contains 766 MB of empty pages, closing the app, checkpointing the WAL, verifying a backup, running &lt;code&gt;VACUUM&lt;/code&gt;, and getting your disk space back.&lt;/p&gt;

&lt;p&gt;That is not glamorous.&lt;/p&gt;

&lt;p&gt;It is exactly the kind of boring tool I want more of.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading. I build tools for AI coding agents at &lt;a href="https://github.com/chncaesar" rel="noopener noreferrer"&gt;github.com/chncaesar&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-db-clean" rel="noopener noreferrer"&gt;opencode-db-clean&lt;/a&gt; — reclaim GBs of SQLite disk space&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-waitfor" rel="noopener noreferrer"&gt;opencode-waitfor&lt;/a&gt; — proper readiness checks, no more sleep loops&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-session-reflection" rel="noopener noreferrer"&gt;opencode-session-reflection&lt;/a&gt; — turn past sessions into workflow improvements&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-fleet" rel="noopener noreferrer"&gt;opencode-fleet&lt;/a&gt; — multi-node OpenCode orchestration&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>database</category>
      <category>programming</category>
    </item>
    <item>
      <title>OpenCode Tool Calling Internals</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:42:59 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/opencode-tool-calling-internals-5gda</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/opencode-tool-calling-internals-5gda</guid>
      <description>&lt;p&gt;This document analyzes OpenCode's tool-calling implementation from the perspective of someone building an agent runtime. The goal is not only to explain how OpenCode happens to call tools today. The more useful question is: what design problems does a real coding-agent runtime have to solve once tool calling moves beyond a demo callback?&lt;/p&gt;

&lt;p&gt;Primary code references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/tool/tool.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/tool/registry.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/tools.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/prompt.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/processor.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/llm.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/llm/ai-sdk.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/session/llm/native-runtime.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/opencode/src/permission/index.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conclusions below follow the normal OpenCode session tool path. MCP tools, plugin tools, and the experimental native LLM runtime are included where they reveal the underlying design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you are building an agent runtime, treat tool calling as a runtime subsystem, not as a provider feature.&lt;/strong&gt; Provider function calling is only the provider-side way of delivering a tool request. The runtime still has to decide which tools exist, which tools are visible, whether execution is allowed, how progress is represented, how output is normalized, and how the session recovers when execution fails.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep an internal tool contract.&lt;/strong&gt; Do not let OpenAI, Anthropic, AI SDK, MCP, or any one provider define the shape of your runtime tools. OpenCode uses &lt;code&gt;Tool.Def&lt;/code&gt; as its internal contract and projects it outward later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build a tool catalog, not a static tool list.&lt;/strong&gt; Tool visibility depends on the agent, model, provider, runtime flags, plugins, MCP clients, and permissions. A serious runtime recomputes the catalog for each turn.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Put permission gates inside execution.&lt;/strong&gt; Prompt instructions and UI affordances are not enough. The dangerous action must block at the point where the tool would actually touch the filesystem, shell, network, or external server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Persist tool calls as state.&lt;/strong&gt; A tool call is not just text in the transcript. It has identity, input, progress, output, attachments, timing, error state, and cancellation behavior.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Normalize provider events before they reach the session layer.&lt;/strong&gt; Providers and model runtimes disagree about streamed tool inputs, results, errors, and provider-executed calls. The session layer should consume one event vocabulary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Design for debuggability from the start.&lt;/strong&gt; Tool call IDs, session IDs, message IDs, permission events, plugin hooks, and durable message parts are not extras. They are what let you answer the basic question: what happened?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Tool Calling Needs A Runtime Layer
&lt;/h2&gt;

&lt;p&gt;The smallest tool-calling demo looks simple. The model emits a function name and JSON arguments. The application looks up a local function, runs it, sends the result back, and the model continues.&lt;/p&gt;

&lt;p&gt;That design collapses as soon as the agent becomes useful.&lt;/p&gt;

&lt;p&gt;A coding agent does not merely call &lt;code&gt;getWeather&lt;/code&gt;. It reads files, writes files, applies patches, spawns shell commands, lists project directories, asks questions, starts subtasks, calls MCP servers, and may return images or other attachments. Some tools are safe. Some need approval. Some are available only for certain models. Some are provided by plugins. Some should be hidden from an agent entirely. Some run long enough that the UI needs progress. Some are interrupted halfway through. Some produce outputs too large to fit into the next request.&lt;/p&gt;

&lt;p&gt;At that point, provider function calling is not the system. It is only one input boundary. The real system is the layer that turns a model's request into a permissioned, observable, cancellable, durable operation inside an agent session.&lt;/p&gt;

&lt;p&gt;OpenCode's implementation is useful because it draws that boundary clearly. Tools are not just callback functions passed to &lt;code&gt;streamText(...)&lt;/code&gt;. They pass through an internal contract, a registry, a session adapter, an LLM runtime adapter, and a session processor. That layering has cost, but it solves problems a direct callback design usually discovers too late.&lt;/p&gt;

&lt;h2&gt;
  
  
  How The Pieces Connect
&lt;/h2&gt;

&lt;p&gt;The concrete implementation path is short enough to keep in your head.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SessionPrompt.runLoop(...)&lt;/code&gt; creates the assistant message for the current provider turn. Before calling the model, it asks &lt;code&gt;SessionTools.resolve(...)&lt;/code&gt; to build the tool map for this exact agent, model, session, permission state, and runtime configuration. &lt;code&gt;SessionTools.resolve(...)&lt;/code&gt; pulls tool definitions from &lt;code&gt;ToolRegistry&lt;/code&gt;, adapts them into AI SDK-compatible tools, injects &lt;code&gt;Tool.Context&lt;/code&gt;, and wires execution through plugin hooks and &lt;code&gt;Permission.ask(...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;SessionProcessor.process(...)&lt;/code&gt; calls &lt;code&gt;LLM.stream(...)&lt;/code&gt;. The default runtime passes the prepared tools to AI SDK &lt;code&gt;streamText(...)&lt;/code&gt;. The native runtime uses a different adapter, but still calls back into the same OpenCode-owned tool execution shape. Runtime-specific stream events are converted into &lt;code&gt;LLMEvent&lt;/code&gt; values. &lt;code&gt;SessionProcessor&lt;/code&gt; consumes those events and persists text, reasoning, tool calls, tool results, errors, usage, and cleanup state as session message parts.&lt;/p&gt;

&lt;p&gt;The spine is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SessionPrompt.runLoop
-&amp;gt; SessionTools.resolve
-&amp;gt; ToolRegistry.tools
-&amp;gt; LLM.stream
-&amp;gt; LLMAISDK.toLLMEvents or native runtime events
-&amp;gt; SessionProcessor.handleEvent
-&amp;gt; Session.updatePart / Session.updateMessage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That path is also the debugging path. If a tool was missing, start at &lt;code&gt;ToolRegistry.tools(...)&lt;/code&gt;. If it was visible but executed with the wrong context, inspect &lt;code&gt;SessionTools.resolve(...)&lt;/code&gt;. If the provider emitted unexpected tool events, inspect the runtime adapter. If the UI shows the wrong state, inspect what &lt;code&gt;SessionProcessor&lt;/code&gt; persisted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Principle 1: Keep A Provider-Independent Tool Contract
&lt;/h2&gt;

&lt;p&gt;OpenCode's internal contract lives in &lt;code&gt;packages/opencode/src/tool/tool.ts&lt;/code&gt;. A tool definition has an ID, description, parameter schema, optional JSON Schema, execute function, and optional validation-error formatter. The important detail is that this is OpenCode's shape, not the provider's shape.&lt;/p&gt;

&lt;p&gt;The parameter schema is an Effect schema. Before any tool implementation runs, the wrapper created by &lt;code&gt;Tool.define(...)&lt;/code&gt; decodes unknown model input. If the model emits invalid arguments, OpenCode raises &lt;code&gt;InvalidArgumentsError&lt;/code&gt; with a model-facing message that asks the model to rewrite the input. Only decoded input reaches the tool implementation.&lt;/p&gt;

&lt;p&gt;The execute function receives a &lt;code&gt;Tool.Context&lt;/code&gt; containing the session ID, message ID, tool call ID, active agent, abort signal, prior messages, a &lt;code&gt;metadata(...)&lt;/code&gt; updater, and an &lt;code&gt;ask(...)&lt;/code&gt; permission hook. That context is the real API a tool author uses. A file-writing tool does not need to know how the TUI handles permission prompts. It only needs to call &lt;code&gt;ctx.ask(...)&lt;/code&gt;. A long-running tool does not need to know how session parts are stored. It only needs to call &lt;code&gt;ctx.metadata(...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The wrapper also enforces cross-cutting behavior. It validates arguments, records a &lt;code&gt;Tool.execute&lt;/code&gt; span, applies output truncation when the tool has not already done so, and attaches common attributes such as &lt;code&gt;tool.name&lt;/code&gt;, &lt;code&gt;session.id&lt;/code&gt;, &lt;code&gt;message.id&lt;/code&gt;, and &lt;code&gt;tool.call_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The design lesson is simple: define the tool contract around your runtime's invariants. Provider schemas, AI SDK tools, MCP definitions, and plugin APIs should be projections or adapters. They should not be the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Principle 2: Build A Runtime Tool Catalog
&lt;/h2&gt;

&lt;p&gt;OpenCode's &lt;code&gt;ToolRegistry&lt;/code&gt; is not just a map from names to functions. It is a catalog builder.&lt;/p&gt;

&lt;p&gt;It starts with built-in tools such as &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;glob&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;webfetch&lt;/code&gt;, &lt;code&gt;todo&lt;/code&gt;, &lt;code&gt;skill&lt;/code&gt;, &lt;code&gt;shell&lt;/code&gt;, and &lt;code&gt;apply_patch&lt;/code&gt;. It then scans configured directories for project tools under &lt;code&gt;{tool,tools}/*.{js,ts}&lt;/code&gt; and loads plugin-provided tools from the plugin service. When a plugin provides a tool, the registry immediately wraps it in OpenCode's internal tool contract, so the rest of the runtime can treat it like a built-in tool.&lt;/p&gt;

&lt;p&gt;The registry then filters and shapes that catalog for the current turn. For example, the web search tool is not always visible. OpenCode exposes it only when the selected provider supports it, or when runtime flags such as Exa or parallel search are enabled. Experimental tools appear only when flags allow them. GPT-family model handling can choose &lt;code&gt;apply_patch&lt;/code&gt; and hide &lt;code&gt;edit&lt;/code&gt; and &lt;code&gt;write&lt;/code&gt;. The &lt;code&gt;task&lt;/code&gt; tool also gets a dynamic description listing the subagents the current agent is allowed to call, so the model knows which &lt;code&gt;subagent_type&lt;/code&gt; values are valid. Before any tool is exposed to the model, plugins also get a &lt;code&gt;tool.definition&lt;/code&gt; hook that can adjust its description or schema.&lt;/p&gt;

&lt;p&gt;This is also how OpenCode exposes subagent delegation. A subagent is not normally launched by a hidden scheduler. The model sees the &lt;code&gt;task&lt;/code&gt; tool, reads the dynamically listed &lt;code&gt;subagent_type&lt;/code&gt; options, and decides whether delegation is useful for the current turn. If it calls &lt;code&gt;task&lt;/code&gt;, the runtime executes that tool and starts the selected subagent. In other words, delegation itself is modeled as tool calling.&lt;/p&gt;

&lt;p&gt;Permissions also affect visibility. A broad deny rule can hide a tool before the model ever sees it. Edit-like tools are grouped under &lt;code&gt;edit&lt;/code&gt;, while MCP resource tools are grouped under &lt;code&gt;read&lt;/code&gt;. This avoids advertising tools that are already forbidden by policy.&lt;/p&gt;

&lt;p&gt;The design lesson is that an agent's tool list is a runtime projection. It depends on who the agent is, what model is running, what session permissions apply, what plugins are installed, what external servers are connected, and what feature flags are enabled. If you make the tool list static, those concerns will leak into individual tools or into prompt text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Principle 3: Put Permissions Inside Execution
&lt;/h2&gt;

&lt;p&gt;OpenCode's permission boundary is not a sentence in the system prompt. It is an Effect that can block tool execution.&lt;/p&gt;

&lt;p&gt;Tools call &lt;code&gt;ctx.ask(...)&lt;/code&gt; with a permission name, path or resource patterns, metadata, and optional &lt;code&gt;always&lt;/code&gt; patterns. The write tool asks for &lt;code&gt;edit&lt;/code&gt; permission and includes a diff. The shell tool parses commands, resolves paths, asks for &lt;code&gt;bash&lt;/code&gt;, and separately asks for &lt;code&gt;external_directory&lt;/code&gt; when a command reaches outside the project boundary. Read-like tools ask for &lt;code&gt;read&lt;/code&gt; over path patterns. MCP resource tools ask for &lt;code&gt;read&lt;/code&gt; over &lt;code&gt;mcp:server:uri&lt;/code&gt; patterns.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Permission.ask(...)&lt;/code&gt; checks three sources of authority: the agent's configured permission rules, any session-level permission overrides, and approvals granted earlier in the current runtime process when the user chose an "always allow" option. If every pattern is allowed, execution continues. If any pattern is denied, execution fails. If the runtime needs user input, it creates a pending request, publishes &lt;code&gt;Permission.Event.Asked&lt;/code&gt;, and waits on a &lt;code&gt;Deferred&lt;/code&gt;. When the client replies, &lt;code&gt;Permission.reply(...)&lt;/code&gt; publishes &lt;code&gt;Permission.Event.Replied&lt;/code&gt; and resolves or rejects the waiting tool.&lt;/p&gt;

&lt;p&gt;This design matters because permission is enforced at the point of action. A model can ask for a shell command. The UI can render a prompt. But the shell command does not run until the permission service resolves. The approval path is part of execution, not decoration around it.&lt;/p&gt;

&lt;p&gt;The design lesson is to make safety a blocking dependency of side effects. If permission is implemented only as model instruction, client UI, or precomputed filtering, eventually some tool path will bypass it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Principle 4: Persist Tool Calls As State
&lt;/h2&gt;

&lt;p&gt;A tool call is a long-lived operation, not a line of assistant text. It begins when the provider says the model wants a tool, but it may pass through streamed input, permission waiting, execution, progress updates, output truncation, attachment handling, cancellation, and failure cleanup before the model sees a result. If a runtime only stores the final tool result as transcript text, it loses the operation boundary.&lt;/p&gt;

&lt;p&gt;That boundary matters. The UI needs to show that a tool is running before it finishes. A permission rejection needs to be different from a tool crash. A cancelled run needs to mark unfinished calls instead of leaving them as ghosts. A debug trace needs to answer which tool ran, with what input, under which assistant message, and what output or error it produced. None of that can be reliably reconstructed from prose.&lt;/p&gt;

&lt;p&gt;Durability adds another benefit: interruption and restart become tractable. If tool calls are persisted as structured records, a runtime can inspect what was pending, running, completed, or failed after a crash or cancellation. That does not mean it should blindly re-execute tools after restart; tools may have side effects. It means the runtime has enough state to make an explicit recovery decision: retry only if safe, mark an abandoned call as interrupted, or rebuild the model-visible history with accurate completed results. Without durable tool state, restart logic has to infer too much from partially written text.&lt;/p&gt;

&lt;p&gt;OpenCode handles this by persisting tool calls as assistant message parts. &lt;code&gt;SessionProcessor&lt;/code&gt; owns the state machine. When the normalized event stream reports tool input or a tool call, the processor calls &lt;code&gt;ensureToolCall(...)&lt;/code&gt;. If this is the first event for a call ID, it creates a &lt;code&gt;tool&lt;/code&gt; part with state &lt;code&gt;pending&lt;/code&gt;. When the &lt;code&gt;tool-call&lt;/code&gt; event arrives, the part becomes &lt;code&gt;running&lt;/code&gt; and records the tool name, input, timing, and provider metadata. When a result arrives, &lt;code&gt;completeToolCall(...)&lt;/code&gt; writes &lt;code&gt;completed&lt;/code&gt; with output, metadata, title, attachments, and end time. When an error arrives, &lt;code&gt;failToolCall(...)&lt;/code&gt; writes an &lt;code&gt;error&lt;/code&gt; state.&lt;/p&gt;

&lt;p&gt;The basic state model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pending
-&amp;gt; running
-&amp;gt; completed | error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also cleanup behavior. At the end of processing, unresolved tool calls are given a short chance to settle. Remaining calls are marked as interrupted errors with &lt;code&gt;Tool execution aborted&lt;/code&gt; and metadata indicating interruption. This keeps the transcript from containing permanently running calls after cancellation or stream failure.&lt;/p&gt;

&lt;p&gt;The exact state machine is small, but the design choice is large. Tool calls are durable operation records. Text is what the model reads later. State is what the runtime needs to render, cancel, inspect, retry, and debug the operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Principle 5: Normalize At Runtime Boundaries
&lt;/h2&gt;

&lt;p&gt;An agent runtime has two unstable boundaries around tool calling. One boundary faces the model provider. The other faces tool implementations. Both are messy.&lt;/p&gt;

&lt;p&gt;Providers do not all describe tool activity the same way. One runtime may stream tool input deltas. Another may report only a completed call. A provider may mark a call as provider-executed, surface a tool error as a stream event, or wrap usage and metadata in provider-specific fields. If those event shapes leak directly into session state, every downstream system has to understand provider quirks: UI rendering, debugging, retries, compaction, permissions, and persistence.&lt;/p&gt;

&lt;p&gt;Tool outputs have the same problem from the other side. One tool may return a string. Another may return a structured object. An MCP tool may return text, an image, or a resource blob. A file-oriented tool may return attachments. A shell tool may return too much output. If the runtime lets every output shape flow straight into history, the next model request becomes unpredictable and the UI has no stable contract to render.&lt;/p&gt;

&lt;p&gt;OpenCode handles the provider side with an event normalization seam. The default runtime path uses AI SDK &lt;code&gt;streamText(...)&lt;/code&gt;; the experimental native runtime lowers selected requests into &lt;code&gt;@opencode-ai/llm&lt;/code&gt;. The session processor does not consume either runtime directly. Both paths converge on &lt;code&gt;LLMEvent&lt;/code&gt;. For the AI SDK path, &lt;code&gt;packages/opencode/src/session/llm/ai-sdk.ts&lt;/code&gt; converts &lt;code&gt;fullStream&lt;/code&gt; events into OpenCode's event vocabulary: text deltas, reasoning events, step events, tool input events, tool calls, tool results, tool errors, provider errors, and finish events. The native runtime emits the same downstream vocabulary.&lt;/p&gt;

&lt;p&gt;OpenCode handles the tool-output side with a normalized result shape: title, metadata, output text, and optional attachments. The tool wrapper applies output truncation unless the tool already reports truncation metadata. &lt;code&gt;SessionTools.resolve(...)&lt;/code&gt; assigns IDs to attachments and binds them to the current session and assistant message. &lt;code&gt;SessionProcessor&lt;/code&gt; can normalize image attachments before persisting the completed result. MCP resource helpers convert text, images, and resource content into explicit text output plus durable file attachments, while unsupported or oversized binary content is omitted with a visible explanation.&lt;/p&gt;

&lt;p&gt;The design choice is not just convenience. It protects the rest of the runtime from two sources of drift: provider event drift and tool output drift. Once a provider stream has become &lt;code&gt;LLMEvent&lt;/code&gt;, session persistence does not care whether the call came from AI SDK or the native runtime. Once a tool result has become OpenCode's output shape, the model, UI, and database do not care whether it came from a built-in tool, plugin tool, or MCP server.&lt;/p&gt;

&lt;p&gt;The lesson for runtime builders is to normalize at every boundary where external variability enters. Do not let provider-specific event shapes or tool-specific output shapes become your session model. Convert them into runtime-owned contracts before they become history, UI state, or the next model input.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OpenCode Gets Right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;It separates internal tool semantics from provider mechanics.&lt;/strong&gt; &lt;code&gt;Tool.Def&lt;/code&gt; is the source of truth. AI SDK tools, MCP tools, plugin tools, and native runtime tools are adapters around that contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It makes tool visibility contextual.&lt;/strong&gt; The runtime does not pretend every agent and every model have the same capabilities. The tool catalog is rebuilt for the current session turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It puts permission checks where side effects happen.&lt;/strong&gt; Sensitive operations block inside execution, and the approval flow is represented as runtime events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It persists tool lifecycle explicitly.&lt;/strong&gt; A tool call has state, input, timing, output, metadata, attachments, and failure information. This is essential for UI, debugging, and recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It creates a stable event seam between model runtimes and session state.&lt;/strong&gt; AI SDK and native runtime differences are normalized before &lt;code&gt;SessionProcessor&lt;/code&gt; handles them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It leaves room for extension.&lt;/strong&gt; Plugins and MCP servers can add tools without bypassing the same broad execution framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where The Design Is Still Expensive
&lt;/h2&gt;

&lt;p&gt;The cost of this design is complexity.&lt;/p&gt;

&lt;p&gt;A tool call can pass through many layers: tool definition, registry projection, provider schema transformation, session adapter, permission gate, plugin hooks, LLM runtime, event normalization, and session processor persistence. When behavior is wrong, the bug may live in any one of those seams. A tool can be defined correctly but hidden by permissions. It can be visible but transformed into a provider schema the model handles poorly. It can execute successfully but return an output shape that is later truncated. It can be interrupted after the provider already emitted a partial event.&lt;/p&gt;

&lt;p&gt;The design also relies on tools asking for the right permissions. The framework can block &lt;code&gt;ctx.ask(...)&lt;/code&gt;, but it cannot magically know which resources an opaque tool input will touch. The shell tool is the clearest example. A file tool receives a structured &lt;code&gt;filePath&lt;/code&gt;, so it can ask for permission over that path directly. A shell tool receives an opaque command string. Before it can ask precise permissions, it has to parse that string, identify filesystem-related commands such as &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;mkdir&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, or &lt;code&gt;cd&lt;/code&gt;, extract path arguments, expand home and environment references, resolve relative paths against the working directory, and detect paths outside the project boundary. Only then can it ask for permissions such as &lt;code&gt;bash&lt;/code&gt; or &lt;code&gt;external_directory&lt;/code&gt; with meaningful patterns. Without that parsing step, the runtime would either approve every shell command too broadly or block useful commands too often.&lt;/p&gt;

&lt;p&gt;Safety moves from a prompt problem to a tool implementation problem. That is better, but it is still work.&lt;/p&gt;

&lt;p&gt;Provider behavior remains an input boundary. OpenCode normalizes provider events after it receives them, but it still depends on the provider or runtime to surface tool calls, results, errors, and cancellation in a usable way. The native runtime can reduce some provider coupling, but it does not remove the need for careful adapter design.&lt;/p&gt;

&lt;p&gt;The design lesson is not that every agent runtime should copy OpenCode's exact files. The lesson is that once an agent can change a real project, tool calling becomes infrastructure. Infrastructure has seams, state, and failure modes.&lt;/p&gt;

&lt;p&gt;The provider tells you what the model wants. The runtime is responsible for making that request safe, observable, and recoverable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading. I build tools for AI coding agents at &lt;a href="https://github.com/chncaesar" rel="noopener noreferrer"&gt;github.com/chncaesar&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-db-clean" rel="noopener noreferrer"&gt;opencode-db-clean&lt;/a&gt; — reclaim GBs of SQLite disk space&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-waitfor" rel="noopener noreferrer"&gt;opencode-waitfor&lt;/a&gt; — proper readiness checks, no more sleep loops&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-session-reflection" rel="noopener noreferrer"&gt;opencode-session-reflection&lt;/a&gt; — turn past sessions into workflow improvements&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-fleet" rel="noopener noreferrer"&gt;opencode-fleet&lt;/a&gt; — multi-node OpenCode orchestration&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>architecture</category>
      <category>llm</category>
      <category>typescript</category>
    </item>
    <item>
      <title>OpenCode V2 Compaction Internals</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:26:41 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/opencode-v2-compaction-internals-2a5d</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/opencode-v2-compaction-internals-2a5d</guid>
      <description>&lt;p&gt;This document analyzes the OpenCode V2 compaction implementation. The conclusions are based on the V2/core code path in the OpenCode repository. When code, comments, and documentation disagree, this document follows the current code behavior.&lt;/p&gt;

&lt;p&gt;Primary code references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/compaction.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/runner/llm.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/history.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/context-epoch.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/runner/to-llm-message.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/session/message-updater.ts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/core/src/config/compaction.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This document only covers the V2/core compaction path. It does not cover the V1 compatibility &lt;code&gt;/compact&lt;/code&gt; implementation or the DCP plugin's &lt;code&gt;compress&lt;/code&gt; tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OpenCode V2 compaction is a durable checkpoint-and-retry mechanism, not a relevance-pruning system.&lt;/strong&gt; It is a last-resort survival mechanism — avoid triggering it whenever possible.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Avoid compaction.&lt;/strong&gt; Once triggered, all older context is compressed into a hardcoded &lt;code&gt;4_096&lt;/code&gt;-token summary. Details that do not fit are not recovered. The session survives, but its continuity depends entirely on what the summary captured.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compaction is size-triggered, not relevance-triggered.&lt;/strong&gt; It runs when the request estimate exceeds the context threshold — regardless of how important or irrelevant the conversation is. The system has no concept of message importance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Three hardcoded limits control quality.&lt;/strong&gt; Tool/shell output is truncated at &lt;code&gt;2_000&lt;/code&gt; chars (&lt;code&gt;TOOL_OUTPUT_MAX_CHARS&lt;/code&gt;), summary output is capped at &lt;code&gt;4_096&lt;/code&gt; tokens (&lt;code&gt;SUMMARY_OUTPUT_TOKENS&lt;/code&gt;), and neither is configurable. Only &lt;code&gt;buffer&lt;/code&gt; and &lt;code&gt;keep.tokens&lt;/code&gt; are exposed in the config schema.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;History is never deleted, but old rows are no longer loaded.&lt;/strong&gt; Old messages remain in durable storage. After compaction, future model requests start from the checkpoint and do not see older rows through normal provider calls.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Media content is not preserved in model-visible context after compaction.&lt;/strong&gt; Images and videos from before the checkpoint are reduced to text metadata (&lt;code&gt;[Attached image/png: screenshot.png]&lt;/code&gt;). The original binary data remains in durable history, but the model cannot see visual content from before the checkpoint through normal provider requests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compaction is safe but blunt.&lt;/strong&gt; It never deletes history, never leaves a half-compacted session, and prevents infinite retry loops. But it makes no attempt to distinguish important facts from noise when summarizing older context.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Core Design
&lt;/h2&gt;

&lt;p&gt;Long coding-agent sessions produce long conversation histories. Tool calls, tool results, shell outputs, file reads, and multi-turn reasoning accumulate. Eventually the full provider request exceeds the model's context limit. Deleting old messages would lose durable history. Compaction solves this with a checkpoint strategy: create a new message that represents older history, then let future runner attempts use that message as a starting boundary.&lt;/p&gt;

&lt;p&gt;V2 compaction separates three layers that would otherwise be conflated:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;What compaction does to it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Durable history&lt;/td&gt;
&lt;td&gt;Full session record in &lt;code&gt;SessionMessageTable&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not changed. Old rows remain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active history&lt;/td&gt;
&lt;td&gt;The slice of messages loaded for a provider attempt&lt;/td&gt;
&lt;td&gt;Shortened. Future loads start from the latest &lt;code&gt;type: "compaction"&lt;/code&gt; checkpoint.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model-visible context&lt;/td&gt;
&lt;td&gt;What the model actually receives in a request&lt;/td&gt;
&lt;td&gt;Replaced. Older context becomes &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; + &lt;code&gt;&amp;lt;recent-context&amp;gt;&lt;/code&gt; inside a &lt;code&gt;&amp;lt;conversation-checkpoint&amp;gt;&lt;/code&gt; block.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This three-layer design is the key insight. Compaction does not shrink the database. It changes the default projection of history used to call the model, and it renders old context through a generated summary rather than replaying every old message.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Trigger
&lt;/h3&gt;

&lt;p&gt;Compaction has two trigger paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic path.&lt;/strong&gt; Before each normal assistant-response provider attempt, the session runner estimates the full provider request size and compares it against the current model's context window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;estimated_request_tokens &amp;gt; model_context_limit - max(output_tokens, compaction_buffer)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default compaction buffer is &lt;code&gt;20_000&lt;/code&gt; tokens. With a &lt;code&gt;128_000&lt;/code&gt; token context window and default output limit, the threshold is approximately &lt;code&gt;108_000&lt;/code&gt; tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recovery path.&lt;/strong&gt; If the provider returns a context overflow error and the current turn has not yet produced durable assistant output or tool execution, OpenCode runs one recovery compaction and retries. This path handles cases where the local token estimate did not prevent a provider-side overflow. The overflow recovery path runs at most once to avoid looping.&lt;/p&gt;

&lt;p&gt;Both paths eventually call the same implementation: &lt;code&gt;compactAfterOverflow(...)&lt;/code&gt; in &lt;code&gt;packages/core/src/session/compaction.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Split History Into &lt;code&gt;head&lt;/code&gt; And &lt;code&gt;recent&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Compaction divides active session history into two parts.&lt;/p&gt;

&lt;p&gt;First, existing &lt;code&gt;type: "compaction"&lt;/code&gt; messages are ignored. The remaining structured session messages are converted into plain text. The helper function is named &lt;code&gt;serialize(...)&lt;/code&gt; in the code, but this is a text conversion step, not binary serialization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user messages become &lt;code&gt;[User]: ...&lt;/code&gt; plus attachment descriptions.&lt;/li&gt;
&lt;li&gt;assistant text becomes &lt;code&gt;[Assistant]: ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;assistant reasoning becomes &lt;code&gt;[Assistant reasoning]: ...&lt;/code&gt; when reasoning text exists.&lt;/li&gt;
&lt;li&gt;assistant tool calls become &lt;code&gt;[Assistant tool call]: tool(input)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;completed tool results become &lt;code&gt;[Tool result]: ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;failed tool calls become &lt;code&gt;[Tool error]: ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;system updates become &lt;code&gt;[System update]: ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;synthetic context becomes &lt;code&gt;[Synthetic context]: ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;shell messages become &lt;code&gt;[Shell]: command + output&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tool output and shell output are truncated to &lt;code&gt;2_000&lt;/code&gt; characters. This limit is hardcoded as &lt;code&gt;TOOL_OUTPUT_MAX_CHARS&lt;/code&gt; in &lt;code&gt;packages/core/src/session/compaction.ts:14&lt;/code&gt; and is not exposed through the compaction config schema. Binary and media content is not embedded as base64. Images, videos, and attachments are reduced to text metadata such as &lt;code&gt;[Attached image/png: screenshot.png]&lt;/code&gt;. The compaction summary request is always text-only. A non-multimodal model can still run the summary request because it receives only text labels. However, it cannot infer visual or binary content unless that content was already described elsewhere in text.&lt;/p&gt;

&lt;p&gt;The converted text is then split by walking backward from the latest message. The default recent-context budget is &lt;code&gt;8_000&lt;/code&gt; tokens from &lt;code&gt;DEFAULT_KEEP_TOKENS&lt;/code&gt;. Recent converted text within this budget is kept as &lt;code&gt;recent&lt;/code&gt;. Older converted text becomes &lt;code&gt;head&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The newly selected &lt;code&gt;recent&lt;/code&gt; is not summarized in the same compaction run. It is stored directly in the checkpoint and later rendered as &lt;code&gt;&amp;lt;recent-context&amp;gt;&lt;/code&gt;. The newly selected &lt;code&gt;head&lt;/code&gt; is what gets summarized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate The Summary
&lt;/h3&gt;

&lt;p&gt;The summary prompt is built by &lt;code&gt;buildPrompt(...)&lt;/code&gt;. Its job is not just to shorten text. It tries to preserve the working state needed for a coding-agent session to continue.&lt;/p&gt;

&lt;p&gt;When there is no previous checkpoint, the prompt starts with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create a new anchored summary from the conversation history.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a previous checkpoint exists, the prompt asks the model to update the anchored summary rather than blindly stacking independent summaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update the anchored summary below using the conversation history above.
Preserve still-true details, remove stale details, and merge in the new facts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt then includes a fixed Markdown template tuned for session recovery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;## Objective&lt;/code&gt; — what the user is trying to accomplish.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;## Important Details&lt;/code&gt; — constraints, decisions, assumptions, exact context needed to continue.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;## Work State&lt;/code&gt; — completed work, active work, blockers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;## Next Move&lt;/code&gt; — the next concrete action after retry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;## Relevant Files&lt;/code&gt; — file paths that would otherwise be easy to lose during summarization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt rules require the model to keep every section, use terse bullets, preserve exact paths/symbols/commands/errors/URLs/identifiers, and avoid mentioning that context was compacted. The last rule matters: the generated checkpoint should read like normal historical context, not an explanation of an internal maintenance operation.&lt;/p&gt;

&lt;p&gt;The context passed into the summary is: previous checkpoint &lt;code&gt;recent&lt;/code&gt;, when one exists, plus the newly selected &lt;code&gt;head&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The summary request uses the same &lt;code&gt;input.model&lt;/code&gt; as the current provider attempt, with no tools and a maximum of &lt;code&gt;4_096&lt;/code&gt; output tokens. This limit is hardcoded as &lt;code&gt;SUMMARY_OUTPUT_TOKENS&lt;/code&gt; in &lt;code&gt;packages/core/src/session/compaction.ts:15&lt;/code&gt; and is not exposed through the compaction config schema. Even if the model supports larger output, the cap is always &lt;code&gt;4_096&lt;/code&gt;. There is no separate specialist compaction model in the current V2 core path.&lt;/p&gt;

&lt;p&gt;This is a significant constraint. A long session may have accumulated detailed information — file paths, commands, error messages, design decisions, constraints, test results — across many turns. All of that older context must be compressed into at most 4,096 output tokens of structured summary. If important details exceed what the summary model can fit, they are not preserved for future provider attempts. The session does not crash, but the model's working knowledge of old context can become incomplete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Persist The Checkpoint
&lt;/h3&gt;

&lt;p&gt;Before summary generation starts, OpenCode publishes &lt;code&gt;SessionEvent.Compaction.Started&lt;/code&gt;. This event does not create a checkpoint message.&lt;/p&gt;

&lt;p&gt;During generation, the implementation collects text delta chunks. If the stream reports a provider error, throws &lt;code&gt;LLM.Error&lt;/code&gt;, or produces an empty summary, compaction returns &lt;code&gt;false&lt;/code&gt; and no checkpoint is written. Failure handling is conservative: a failed summary attempt does not move the active-history boundary.&lt;/p&gt;

&lt;p&gt;Only after a non-empty summary is generated does OpenCode publish &lt;code&gt;SessionEvent.Compaction.Ended&lt;/code&gt;. The session projector handles this event by inserting a durable &lt;code&gt;type: "compaction"&lt;/code&gt; message into &lt;code&gt;SessionMessageTable&lt;/code&gt;. The inserted row includes the message ID, session ID, compaction type, event sequence, creation time, and the payload: &lt;code&gt;reason&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, and &lt;code&gt;recent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the point where the generated summary becomes part of session history and can be used as the next active-history boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry From The Checkpoint
&lt;/h3&gt;

&lt;p&gt;After the checkpoint is persisted, &lt;code&gt;compactAfterOverflow(...)&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt;, and &lt;code&gt;compactIfNeeded(...)&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; to the runner. The runner stops the current attempt before calling the model by throwing &lt;code&gt;ContinueAfterCompaction&lt;/code&gt; — a control-flow signal, not a user-facing error.&lt;/p&gt;

&lt;p&gt;On the retry, OpenCode reloads active history from the latest compaction checkpoint and rebuilds a smaller provider request. That smaller request is then sent to the model.&lt;/p&gt;

&lt;p&gt;The normal path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build request -&amp;gt; estimate size -&amp;gt; split history -&amp;gt; summarize head -&amp;gt; write checkpoint -&amp;gt; retry -&amp;gt; call model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Repeated Compaction
&lt;/h3&gt;

&lt;p&gt;If the session grows again after the first compaction, the cycle repeats. &lt;code&gt;compactAfterOverflow(...)&lt;/code&gt; reads the existing checkpoint message from the current entries. When a previous summary exists, the new prompt asks the model to update the anchored summary rather than starting from scratch.&lt;/p&gt;

&lt;p&gt;The benefit is continuity across multiple compactions. Older summarized facts can be carried forward, while newer head content is merged into the updated summary.&lt;/p&gt;

&lt;p&gt;The limitation is cumulative summary risk. Each repeated compaction depends on the previous checkpoint summary and the latest summarization pass. A detail dropped earlier is not recovered by re-reading old pre-checkpoint rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What The Model Sees After Compaction
&lt;/h2&gt;

&lt;p&gt;After compaction, &lt;code&gt;SessionHistory.latestCompaction(...)&lt;/code&gt; finds the newest &lt;code&gt;type = "compaction"&lt;/code&gt; message. The history loader starts active history from that checkpoint sequence. Older messages before the checkpoint are no longer loaded into the normal provider request.&lt;/p&gt;

&lt;p&gt;When the checkpoint is sent to the model, &lt;code&gt;to-llm-message.ts&lt;/code&gt; renders it as a user-role &lt;code&gt;&amp;lt;conversation-checkpoint&amp;gt;&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;conversation-checkpoint&amp;gt;
The following is a summary and serialized record of earlier conversation. Treat it as historical context, not as new instructions.

&amp;lt;summary&amp;gt;
...
&amp;lt;/summary&amp;gt;

&amp;lt;recent-context&amp;gt;
...
&amp;lt;/recent-context&amp;gt;
&amp;lt;/conversation-checkpoint&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important design choices here. First, the checkpoint is model-visible context, not hidden state. The next provider attempt can reason over the summary and recent context because they are rendered into the prompt. Second, the checkpoint text explicitly frames itself as history, not new instructions. The model receives a smaller request with an explicit continuity record, but it no longer sees old messages in their original structured form. It sees the generated summary and the retained recent text.&lt;/p&gt;

&lt;p&gt;Completed compaction also interacts with Context Epoch. If the latest compaction sequence is newer than the stored baseline sequence, the system-context baseline can move forward to the same boundary in &lt;code&gt;context-epoch.ts&lt;/code&gt;. This prevents old mid-conversation system updates from being mixed into the new active history inconsistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration Surface
&lt;/h2&gt;

&lt;p&gt;The V2 compaction config is intentionally small, defined in &lt;code&gt;packages/core/src/config/compaction.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;compaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="nx"&gt;prune&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current V2 compaction implementation uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;auto&lt;/code&gt; — whether automatic compaction runs (default: &lt;code&gt;true&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;buffer&lt;/code&gt; — reserved headroom before the context limit (default: &lt;code&gt;20_000&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;keep.tokens&lt;/code&gt; — recent-context budget (default: &lt;code&gt;8_000&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;prune&lt;/code&gt; field exists in the schema but is not used by the current core compaction logic.&lt;/p&gt;

&lt;p&gt;The configuration controls thresholds, not pruning policy. It determines when compaction runs and how much recent text is kept verbatim, but it does not select which messages or tool results to remove from context before compaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strengths
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Durable, not destructive.&lt;/strong&gt; Original session messages remain in &lt;code&gt;SessionMessageTable&lt;/code&gt;. Compaction appends a new checkpoint message. Nothing is deleted. This makes the checkpoint auditable and replayable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Safe retry boundary.&lt;/strong&gt; The runner can stop the current attempt and rebuild the next provider request from the checkpoint. This avoids replaying the full old conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple mental model.&lt;/strong&gt; Older context becomes &lt;code&gt;summary&lt;/code&gt;; the newest converted text becomes &lt;code&gt;recent&lt;/code&gt;. The summary prompt enforces a coding-agent-oriented structure — &lt;code&gt;Objective&lt;/code&gt;, &lt;code&gt;Work State&lt;/code&gt;, &lt;code&gt;Next Move&lt;/code&gt;, &lt;code&gt;Relevant Files&lt;/code&gt; — tuned for session recovery rather than generic prose compression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recent exact context preserved.&lt;/strong&gt; The most recent converted text is not summarized in the same compaction run. It is stored directly as &lt;code&gt;recent&lt;/code&gt;, reducing the chance of losing exact details from the latest work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conservative failure mode.&lt;/strong&gt; If compaction cannot produce a completed checkpoint — summary prompt too large, provider error, empty summary — it does not move the active-history boundary. The session remains in its pre-compaction state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint framed as history, not instruction.&lt;/strong&gt; The &lt;code&gt;&amp;lt;conversation-checkpoint&amp;gt;&lt;/code&gt; text explicitly declares itself historical context, not new instructions. This is simple prompt engineering that reduces the risk of the model misinterpreting the compacted summary as fresh user intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small, predictable configuration surface.&lt;/strong&gt; Only three fields control behavior in the current core path: &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;buffer&lt;/code&gt;, and &lt;code&gt;keep.tokens&lt;/code&gt;. There are no complex pruning policies to tune or debug. The behavior is threshold-driven and easy to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The summary output bottleneck — this is the most important weakness.&lt;/strong&gt; The summary output limit is hardcoded at &lt;code&gt;4_096&lt;/code&gt; tokens (&lt;code&gt;SUMMARY_OUTPUT_TOKENS&lt;/code&gt; in &lt;code&gt;packages/core/src/session/compaction.ts:15&lt;/code&gt;) and not configurable. A long coding session can span tens of thousands of tokens of tool outputs, file reads, error messages, design decisions, shell commands, and reasoning chains. All of that older context — everything except the most recent &lt;code&gt;~8_000&lt;/code&gt; tokens — must be compressed into at most &lt;code&gt;4_096&lt;/code&gt; tokens of structured summary.&lt;/p&gt;

&lt;p&gt;This means any detail that does not fit into the summary is not automatically recovered from durable history during future provider requests. The model's working knowledge of old context becomes only what the summary captured. File paths can be dropped. Error messages can be paraphrased into uselessness. Design decisions can be collapsed to a bullet point. The session does not crash, but its continuity depends entirely on the quality of the 4k-token summary.&lt;/p&gt;

&lt;p&gt;This is the fundamental reason to avoid triggering OpenCode V2 compaction whenever possible. Compaction is a survival mechanism, not an optimization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size-based trigger, not relevance-based.&lt;/strong&gt; Compaction starts when the estimated provider request is too large, or after a provider-side context overflow. It does not run because a message is stale, duplicated, low-value, or off-topic. The system has no concept of message importance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coarse summarization of older context.&lt;/strong&gt; Older converted text is summarized as a block. The implementation does not rank individual messages, tool results, command outputs, or file reads by relevance. It treats all pre-recent history as summarization material.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shallow tool output handling.&lt;/strong&gt; Large tool and shell outputs are truncated to &lt;code&gt;2_000&lt;/code&gt; characters during text conversion. This limit is hardcoded (&lt;code&gt;TOOL_OUTPUT_MAX_CHARS&lt;/code&gt;) and not configurable. A single important line in a large log file may be truncated away, while a verbose but low-value message may consume recent-context budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Media content is not visible to the model after compaction.&lt;/strong&gt; Images, videos, and other binary attachments are reduced to text metadata such as MIME type and filename during text conversion. Once media falls behind the compaction boundary, future model requests see only the checkpoint text. The original binary data remains in durable history, but it is no longer part of model-visible context through normal provider requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checkpoint is irreversible for active context.&lt;/strong&gt; After compaction, future model requests normally depend on the checkpoint summary and recent context for older information. Mistakes, omissions, or distortions in the summary become the model's only working knowledge of old context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No deterministic pruning.&lt;/strong&gt; The config schema includes &lt;code&gt;prune&lt;/code&gt;, but current V2 core compaction does not use it. The implementation is checkpoint summarization, not a policy engine for removing specific tool results, errors, or repeated file reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claims The Current V2 Code Does Not Support
&lt;/h2&gt;

&lt;p&gt;Based on the current V2/core code, do not claim that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;V2 supports user-selected manual compaction ranges.&lt;/li&gt;
&lt;li&gt;V2 uses a hidden &lt;code&gt;compaction&lt;/code&gt; agent.&lt;/li&gt;
&lt;li&gt;V2 uses a separate smaller model for compaction.&lt;/li&gt;
&lt;li&gt;V2 performs deterministic pruning of old tool results.&lt;/li&gt;
&lt;li&gt;V2 keeps the last N turns.&lt;/li&gt;
&lt;li&gt;V2 deletes historical messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reliable claim is more specific: V2 automatically creates a summary checkpoint when a normal provider attempt is too large, or when provider overflow recovery succeeds before side effects.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading. I build tools for AI coding agents at &lt;a href="https://github.com/chncaesar" rel="noopener noreferrer"&gt;github.com/chncaesar&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-db-clean" rel="noopener noreferrer"&gt;opencode-db-clean&lt;/a&gt; — reclaim GBs of SQLite disk space&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-waitfor" rel="noopener noreferrer"&gt;opencode-waitfor&lt;/a&gt; — proper readiness checks, no more sleep loops&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-session-reflection" rel="noopener noreferrer"&gt;opencode-session-reflection&lt;/a&gt; — turn past sessions into workflow improvements&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-fleet" rel="noopener noreferrer"&gt;opencode-fleet&lt;/a&gt; — multi-node OpenCode orchestration&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;A ⭐ on GitHub means a lot!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>llm</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>My Agent Kept Writing sleep Loops. So I Gave It a Better Primitive</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Sat, 11 Jul 2026 02:19:09 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/my-agent-kept-writing-sleep-loops-so-i-gave-it-a-better-primitive-f0j</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/my-agent-kept-writing-sleep-loops-so-i-gave-it-a-better-primitive-f0j</guid>
      <description>&lt;p&gt;I deployed a change, and the agent needed to confirm the new version was live before running a smoke test. So it wrote what agents always write in this situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 40&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;3&lt;span class="p"&gt;;&lt;/span&gt; curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://host/health &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it read the output, saw the old version still serving, and declared the deploy done anyway — because the loop had exited on the first &lt;code&gt;curl&lt;/code&gt; that returned &lt;em&gt;anything&lt;/em&gt;, not the first one that returned the &lt;em&gt;right thing&lt;/em&gt;. I had watched a variant of this play out a dozen times. The agent either polls too few times and gives up early, or hard-codes a &lt;code&gt;sleep 120&lt;/code&gt; and blocks the whole session on a fixed guess, or exits on a 200 that carries a stale body. Every time, it re-derives the same fragile loop from scratch, because there is nothing better sitting in its toolbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why agents reach for sleep
&lt;/h2&gt;

&lt;p&gt;An agent acts through the tools it has. Give it &lt;code&gt;bash&lt;/code&gt;, and "wait until the server is ready" collapses into "sleep and hope," because a fixed sleep is the cheapest thing the shell offers. It has no primitive for &lt;em&gt;readiness&lt;/em&gt; — no verb that means "keep checking this condition, on a sensible cadence, until it holds or you give up." So it fakes one, and the fake is worse in every dimension: it doesn't know how long to wait, it doesn't know what "ready" actually looks like, and when it fails it throws away everything it observed on the way down.&lt;/p&gt;

&lt;p&gt;That last part is the real cost. A hand-rolled loop that times out tells you nothing. The agent is left to run &lt;em&gt;another&lt;/em&gt; probe just to find out why the first forty failed — a human-speed round trip through the clipboard, which is exactly the distance I keep trying to shorten between an agent and the ground truth it can't see.&lt;/p&gt;




&lt;h2&gt;
  
  
  The primitive
&lt;/h2&gt;

&lt;p&gt;So I wrote &lt;a href="https://github.com/chncaesar/opencode-waitfor" rel="noopener noreferrer"&gt;&lt;code&gt;opencode-waitfor&lt;/code&gt;&lt;/a&gt;, a zero-dependency plugin that adds one tool, &lt;code&gt;wait_for&lt;/code&gt;. You install it by adding one line to &lt;code&gt;opencode.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"opencode-waitfor"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool infers what kind of target it's watching. A URL with a scheme gets polled over HTTP; a bare &lt;code&gt;host:port&lt;/code&gt; gets a TCP connection check; anything else runs as a shell command. Three shapes of "is it up yet," one verb:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wait_for http://localhost:3000
wait_for localhost:5432 timeout 10
wait_for http://host/health expect { json_match: { status: ok, version: abc123 } }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is the case that started this. After deploying commit &lt;code&gt;abc123&lt;/code&gt;, the agent waits until &lt;code&gt;/health&lt;/code&gt; reports &lt;em&gt;that&lt;/em&gt; version — not any response, the correct one. The stale-body deploy I opened with simply cannot pass.&lt;/p&gt;

&lt;p&gt;And when it does time out, it returns the last thing it saw: the final HTTP status and body, or the last command's exit code and output. The agent gets to diagnose from the failure it already has, instead of firing a fresh probe to reconstruct it.&lt;/p&gt;




&lt;p&gt;I've written before that the job left to a human, when the agent writes more code than you can read, is standing at the boundary and shortening the distance to ground truth. Usually that's a manual act. Sometimes you can make it structural — hand the agent a primitive shaped like the thing it kept faking, and the bad behavior stops being something you prompt against and starts being something it can't easily do. A tool changes what an agent reaches for more reliably than any instruction telling it to reach for something else.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading. I build tools for AI coding agents at &lt;a href="https://github.com/chncaesar" rel="noopener noreferrer"&gt;github.com/chncaesar&lt;/a&gt;:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-db-clean" rel="noopener noreferrer"&gt;opencode-db-clean&lt;/a&gt; — reclaim GBs of SQLite disk space&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-waitfor" rel="noopener noreferrer"&gt;opencode-waitfor&lt;/a&gt; — proper readiness checks, no more sleep loops&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-session-reflection" rel="noopener noreferrer"&gt;opencode-session-reflection&lt;/a&gt; — turn past sessions into workflow improvements&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://github.com/chncaesar/opencode-fleet" rel="noopener noreferrer"&gt;opencode-fleet&lt;/a&gt; — multi-node OpenCode orchestration&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
