<?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>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>
    <item>
      <title>Six Laws for Talking to AI</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Thu, 02 Jul 2026 01:15:10 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/six-laws-for-talking-to-ai-4dan</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/six-laws-for-talking-to-ai-4dan</guid>
      <description>&lt;p&gt;I recently opened a SQLite file — the local session log from OpenCode, the AI coding tool I use every day. 192 sessions, 8,471 messages, 89 million input tokens. Total cost: \$518.&lt;/p&gt;

&lt;p&gt;But cost per token is the wrong metric. I wanted to know: how much of what I said was wasted?&lt;/p&gt;

&lt;p&gt;So I wrote some queries. I counted every message where I said "不对," "不行," "不是," "不不" — the Chinese equivalents of "no, wrong, not that, stop." I counted sessions where I forked the same conversation and started over. I looked at how many messages were shorter than ten characters. I had six sub-agents analyze the six longest sessions independently, pulling out every instance where I corrected the AI, repeated myself, or sent an instruction so vague the AI had to guess.&lt;/p&gt;

&lt;p&gt;The number that stopped me: &lt;strong&gt;60% of my sessions were forked from an older one.&lt;/strong&gt; I was restarting more conversations than I was finishing. For every two user messages in my worst session, I started a new session.&lt;/p&gt;

&lt;p&gt;The rest of this article is what I found. Six patterns. Six fixes. All measurable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data Snapshot
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total sessions&lt;/td&gt;
&lt;td&gt;192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total messages&lt;/td&gt;
&lt;td&gt;8,471 (7,109 AI, 1,366 me)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions with a parent (forked)&lt;/td&gt;
&lt;td&gt;115 (60%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit correction messages&lt;/td&gt;
&lt;td&gt;67 (4.9% of my messages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Messages mentioning push/commit&lt;/td&gt;
&lt;td&gt;132&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary model&lt;/td&gt;
&lt;td&gt;deepseek-v4-pro (125 sessions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions above 100 messages&lt;/td&gt;
&lt;td&gt;4 (capped at 164)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four sessions hit triple-digit message counts. The longest — a deployment migration to a new server — ran 164 messages. I said "get my approval first" four times in that session alone. I said "use GitHub Actions, not manual commands" five times. No one else was in the room. I was repeating myself to a machine.&lt;/p&gt;

&lt;p&gt;But that 60% fork rate cuts both ways, and I have to be honest about the other side of it. The human in me is lazy. I hate starting new sessions. A fresh session means I have to re-explain the project, re-establish the rules, re-load the mental model. So I don't. I cram unrelated tasks into one session until it becomes a junk drawer. Deployment config, CSS refactoring, database schema changes, and a React component all in the same thread. By message 80, the AI has no idea what we're working on anymore, and neither do I. The context window might technically hold 200K tokens, but attention is not a buffer — it's a spotlight, and my spotlight is painting six walls at once.&lt;/p&gt;

&lt;p&gt;The 60% fork rate isn't a disciplined practice. It's a symptom. I fork when I'm frustrated, and I don't fork when I should. Both are failures of context discipline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 1: The gap between knowing and writing is the whole cost.
&lt;/h2&gt;

&lt;p&gt;Everyone knows AI forgets. The interesting question is not &lt;em&gt;does it forget&lt;/em&gt; — it's &lt;em&gt;how long do you wait before fixing it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Across six sessions, I corrected the AI for the same rule violations 27 times before writing a single line to my config file. Not 27 times across months. 27 times where I already knew the pattern, already had the fix in my head, and just didn't stop to write it down.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Corrected in chat&lt;/th&gt;
&lt;th&gt;Messages between first correction and AGENTS.md write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Get my approval before acting"&lt;/td&gt;
&lt;td&gt;4 times&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"All operations must use GitHub Actions"&lt;/td&gt;
&lt;td&gt;5 times&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Talk business concepts, not code"&lt;/td&gt;
&lt;td&gt;4 times&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Remove project-specific details"&lt;/td&gt;
&lt;td&gt;4 times&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server migration session is the cleanest example. I told the AI "get my approval before acting" on message 1. By message 48, it had forgotten. I said it again. At message 62, the AI pushed code without confirmation — I said it a third time, with profanity. At message 63, ten minutes later, it happened again. I finally wrote the rule to AGENTS.md around message 80.&lt;/p&gt;

&lt;p&gt;The waste wasn't the 4 corrections. The waste was the 79 messages between "I know this should be a rule" and "this is now a rule."&lt;/p&gt;

&lt;p&gt;Every person who uses AI coding tools knows that writing rules fixes things. The thing I didn't know until I counted: &lt;strong&gt;I average 36 messages between knowing and writing.&lt;/strong&gt; I don't have a knowledge problem. I have an execution latency problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: After &lt;em&gt;every single correction&lt;/em&gt;, ask: does this rule apply to future sessions? If yes, write it now. Not after this task. Not after this session. Now. The cost of writing is ten seconds. The cost of not writing is the rest of the session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 2: Assess the blast radius. Every time.
&lt;/h2&gt;

&lt;p&gt;Six sessions. Four tirades. Every one followed the same script: I give an instruction involving modification or deletion. The AI starts changing files without telling me what. Something breaks that I didn't expect. I discover the damage and lose my temper.&lt;/p&gt;

&lt;p&gt;Specifics: rewriting a website footer and deleting the social links in it. Deleting an entire RSS feed when I only asked to filter old articles. Manually running commands on a production server. Claiming a file exists when it doesn't.&lt;/p&gt;

&lt;p&gt;My AGENTS.md already says push and deploy require explicit user confirmation. I wrote that after the server migration disaster. But the rule is too narrow. It only covers git. It doesn't cover rewrites, bulk replacements, folder restructures, or production commands — all of which share the same asymmetry: three seconds to break, thirty minutes to fix.&lt;/p&gt;

&lt;p&gt;Here is the thing I missed: this is not a special AI rule. This is the same principle you apply before any production change with a blast radius larger than one file.&lt;/p&gt;

&lt;p&gt;We don't let teammates push to production without a diff. We don't approve a database migration without reviewing which tables it touches. We don't run &lt;code&gt;terraform apply&lt;/code&gt; without reading the plan first. The AI is no different — except that it's faster and has less judgment. An intern who can type at 10,000 WPM. The engineering discipline that keeps production safe is the same discipline that keeps an AI session from spiraling.&lt;/p&gt;

&lt;p&gt;Before I started counting, I thought "ask the AI to confirm" was about trust. It's not. It's about blast radius. The AI proposed a change to my website footer. One file. What's the worst that could happen? The footer appears on every page. That's 100% of the site. One file, full blast radius. The AI doesn't understand that. You do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Before any operation that touches more than one file or any file with a cross-cutting footprint, the AI must list every file it will touch and every change it will make. Then wait for confirmation. This is not a negotiation with a coworker. It's a pre-flight checklist. The same instinct that makes you read &lt;code&gt;terraform plan&lt;/code&gt; output should make you read the AI's change list. Same discipline. Same muscle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 3: Send the full spec. Not one sentence at a time.
&lt;/h2&gt;

&lt;p&gt;My worst sessions all share a shape: I start with a rough idea, then refine it through seventeen messages. The AI follows each micro-adjustment, but the overhead of each round trip compounds fast.&lt;/p&gt;

&lt;p&gt;Here is a real example, anonymized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"I need a search feature."
"It should filter by name."
"Email too."
"Partial match, not exact."
"Show results in a dropdown."
"Debounce the input by 300ms."
"Start."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven messages. Here is the same spec as one message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search feature: typeahead dropdown. Filter by name and email, partial match.
Debounce input at 300ms. Show results below the search bar.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two messages would have done it. I spent five extra messages hand-holding the specification because I hadn't completed it in my own head before typing.&lt;/p&gt;

&lt;p&gt;This happened in every long session. The paywall discussion took 18 messages (cancel → no, restrict → wait, AI is paid too → defer). The server migration plan changed direction three times (fully migrate → partially migrate → open a new VM instead). None of these decisions were bad. But each mid-flight change of direction forced the AI to recompute context that had already been settled, which meant it forgot things discussed earlier — which meant I had to re-explain them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Before sending a feature request, write the full spec in a text editor. Fields. Constraints. Edge cases. Interactions. Then send it once. The thirty seconds you spend typing to yourself are worth five rounds with the AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 4: An instruction under ten characters is a puzzle.
&lt;/h2&gt;

&lt;p&gt;In my longest technical session — building a rules-checking tool — 72% of my messages were under fifty characters. 19% were under five characters.&lt;/p&gt;

&lt;p&gt;Here is what I actually sent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I typed&lt;/th&gt;
&lt;th&gt;What the AI had to guess&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;change&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Change what? Code? Plan? Naming?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clean up&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clean which directory? Which files?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;do it.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Do which option?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;symbolic link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;From where to where?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;check-rules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run the tool? Check a file? Create it?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each two-word instruction cost one to three clarification rounds. The AI would propose options. I'd pick one. Total waste per instance: two to four messages. In a 74-message session, I estimate these cost me about fifteen rounds.&lt;/p&gt;

&lt;p&gt;The interesting thing is that I knew exactly what I meant when I typed "change." The context was clear to me. The problem is that context lives in my brain, not in the chat. The AI can only act on what's in the text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Before sending, ask: if someone read this instruction with zero prior context, could they execute it? If the answer is no, add a sentence. &lt;code&gt;"change"&lt;/code&gt; becomes &lt;code&gt;"Add input sanitization to the form submission handler."&lt;/code&gt; Same idea. Seven more words. Zero guessing rounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 5: A bug in file A is a bug in files B through Z.
&lt;/h2&gt;

&lt;p&gt;A session where I was deploying multiple services taught me this. Each service had its own CI config file. Service A's deployment failed — missing a network setting. I pasted the error log. The AI fixed Service A.&lt;/p&gt;

&lt;p&gt;Service B's deployment failed. Same error. Different file. I pasted the log. The AI fixed Service B.&lt;/p&gt;

&lt;p&gt;Service C's deployment failed. Same error. I lost my patience. "You didn't listen," I said. "I told you to fix this."&lt;/p&gt;

&lt;p&gt;The AI had never been told to fix all config files. It had been told to fix the one I pasted, twice. I was angry at the AI for doing exactly what I asked.&lt;/p&gt;

&lt;p&gt;A similar thing happened in another project: a validation bug existed in both the frontend and backend. I fixed the backend, deployed, and the error came back. The frontend had the same logic, untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: After identifying a bug in one file, the next instruction should be: "Check all files in this category for the same issue." Not "fix this one." One sentence prevents the slow drip of identical errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 6: "This" and "all" are not the same thing. Say which.
&lt;/h2&gt;

&lt;p&gt;I asked the AI to add input validation to a module. "Validate the email field," I said. The AI validated email — and only email. I meant all fields on the form. But I had said "this field." The AI took me literally.&lt;/p&gt;

&lt;p&gt;Same thing with error handling: "add try-catch to this function" got me one function. I wanted it across the entire module. Three rounds to converge.&lt;/p&gt;

&lt;p&gt;In both cases I had said "this" — this field, this function — when I meant "every field in the module," "every function that calls an external API." The AI applied the constraint to the one thing I named. I thought the context made my intent obvious. It didn't.&lt;/p&gt;

&lt;p&gt;Another case: I told the AI to add rate limiting to one API endpoint, intending it as a pattern for the whole service. The AI added it to one endpoint. I said "no, all of them." Two rounds. If I had started with "add rate limiting to all endpoints," done in one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: If a constraint applies to more than one output, start with "all." If it applies to one, start with "this one." Explicit scope costs zero tokens and saves three corrections.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Compounding Asset
&lt;/h2&gt;

&lt;p&gt;None of these laws are about prompt engineering. They are about systematizing your own communication patterns. The real insight from 192 sessions is not that I needed better prompts. It's that I needed a process for not repeating myself.&lt;/p&gt;

&lt;p&gt;AGENTS.md is not a file you write once. It's a muscle you exercise after every correction. The engineer who talks to AI for a hundred sessions and writes nothing down has the same information as session ten. The one who writes a rule after every correction has a compounding asset. Every new session starts with the accumulated wisdom of every previous correction. The AI catches up to your intent faster. You repeat yourself less. The sessions get shorter, the output higher quality, and the four-letter words drop to zero.&lt;/p&gt;

&lt;p&gt;That's the asymptote worth chasing.&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>coding</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Reverse Engineered a Closed-Format App. Everything Was in SQLite.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Fri, 26 Jun 2026 16:47:14 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/i-reverse-engineered-a-closed-format-app-everything-was-in-sqlite-340g</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/i-reverse-engineered-a-closed-format-app-everything-was-in-sqlite-340g</guid>
      <description>&lt;p&gt;I had accumulated over two thousand notes in Youdao Cloud Note over several years. When I decided to move to Obsidian, the first thing I checked was the export feature. There wasn't one. No batch export, no single-note export, nothing in the Mac client.&lt;/p&gt;

&lt;p&gt;So I went looking for the local data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where The Data Was
&lt;/h2&gt;

&lt;p&gt;On macOS, Youdao Cloud Note stores its data here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/Library/Containers/ynote-desktop/Data/Library/Application Support/ynote-desktop/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside that directory, organized by account email, were three SQLite databases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&amp;lt;&lt;span class="n"&gt;account&lt;/span&gt;&amp;gt;.&lt;span class="n"&gt;db&lt;/span&gt;         &lt;span class="c"&gt;# note metadata, folder hierarchy
&lt;/span&gt;&amp;lt;&lt;span class="n"&gt;account&lt;/span&gt;&amp;gt;-&lt;span class="n"&gt;content&lt;/span&gt;.&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="c"&gt;# note content (old editor)
&lt;/span&gt;&amp;lt;&lt;span class="n"&gt;account&lt;/span&gt;&amp;gt;-&lt;span class="n"&gt;search&lt;/span&gt;.&lt;span class="n"&gt;db&lt;/span&gt;  &lt;span class="c"&gt;# search index
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a &lt;code&gt;file/&lt;/code&gt; directory with 16 subdirectories arranged by the first character of each file ID, holding the new editor's local files. Everything was unencrypted.&lt;/p&gt;




&lt;h2&gt;
  
  
  What The Database Contained
&lt;/h2&gt;

&lt;p&gt;The main database had two critical tables:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;note&lt;/code&gt;&lt;/strong&gt; — the note catalog. Key columns: &lt;code&gt;fileId&lt;/code&gt; (UUID), &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;parentId&lt;/code&gt; (folder reference), &lt;code&gt;orgEditorType&lt;/code&gt; (0 for the new block editor, 1 for the old plain editor), &lt;code&gt;entryPath&lt;/code&gt; (path to the local file), &lt;code&gt;createTime&lt;/code&gt; (Unix timestamp in seconds), and &lt;code&gt;deleted&lt;/code&gt; (NULL meant not deleted).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;note_book&lt;/code&gt;&lt;/strong&gt; — the folder tree. Folders were not in &lt;code&gt;note&lt;/code&gt; at all. They lived in a separate table with their own &lt;code&gt;fileId&lt;/code&gt; and &lt;code&gt;parentId&lt;/code&gt; fields, forming a tree you could traverse with BFS.&lt;/p&gt;

&lt;p&gt;The content database held a &lt;code&gt;contenttable&lt;/code&gt; with a &lt;code&gt;content&lt;/code&gt; field, but the field was truncated to around 150 characters — just enough for search snippets. The real content for old-editor notes lived here too, with variable lengths.&lt;/p&gt;

&lt;p&gt;Where the content actually was depended on which editor wrote the note:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;orgEditorType&lt;/th&gt;
&lt;th&gt;Editor&lt;/th&gt;
&lt;th&gt;Content source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;New (block editor)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;entryPath&lt;/code&gt; → local file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Old (plain)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contenttable.content&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The local files came in two formats: JSON (449 of them) and plain Markdown (1,832).&lt;/p&gt;




&lt;h2&gt;
  
  
  The JSON Block Tree
&lt;/h2&gt;

&lt;p&gt;The new editor stored notes as a block tree in JSON. Each block followed this structure, with the actual keys being cryptic integers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"6" → block type (p=paragraph, h=heading, co=code, t=table, im=image, l=list, q=quote, hr=divider)
"4" → properties (heading level, code language, image URL, etc.)
"5" → array of child blocks
"7" → inline text segments with "9" format markers (b=bold, i=italic, li=link, il=inline code, etc.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking the &lt;code&gt;"5"&lt;/code&gt; array recursively converted the entire tree to Markdown. The core renderer was under 200 lines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Things That Wasted My Time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;__compress__&lt;/code&gt; flag.&lt;/strong&gt; Ten JSON files had &lt;code&gt;"__compress__": true&lt;/code&gt;. I spent an hour trying to uncompress them with LZString before realizing the &lt;code&gt;"5"&lt;/code&gt; array was still just a plain array — not a compressed string. The flag had been set but never used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleted is NULL, not 0.&lt;/strong&gt; Every note with &lt;code&gt;deleted IS NULL&lt;/code&gt; had been a living note. &lt;code&gt;WHERE deleted = 0&lt;/code&gt; returned nothing. This is standard SQL but easy to miss when you're scanning thousands of rows for anomalies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The timestamp is in seconds.&lt;/strong&gt; &lt;code&gt;1484115955&lt;/code&gt; is January 11, 2017. If you treat it as milliseconds and divide by 1000, you land in 1970. I did that once.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Dead-End That Wasn't
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;contenttable.content&lt;/code&gt; field was consistently 150 characters. This looked like a deliberate truncation to defeat extraction. But the actual content was never supposed to be in that column — the new editor stored everything in local files referenced by &lt;code&gt;entryPath&lt;/code&gt;. The content column was just a search index. Once I followed the &lt;code&gt;entryPath&lt;/code&gt; trail, I had the full text of every note.&lt;/p&gt;




&lt;h2&gt;
  
  
  What The Script Does
&lt;/h2&gt;

&lt;p&gt;The whole thing is a single Python file. It connects to the three SQLite databases, walks the folder tree, maps every note to its content source, converts JSON blocks to Markdown when needed, and writes the output organized by folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/chncaesar/youdao-to-obsidian.git
&lt;span class="nb"&gt;cd &lt;/span&gt;youdao-to-obsidian
pip3 &lt;span class="nb"&gt;install &lt;/span&gt;beautifulsoup4
python3 youdao_migrate.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script auto-detects your account and data directory. Output goes to &lt;code&gt;~/Desktop/obsidian/&lt;/code&gt; by default. Each note gets a &lt;code&gt;.md&lt;/code&gt; file with YAML frontmatter preserving the original title, creation date, and source ID.&lt;/p&gt;

&lt;p&gt;I also bundled a Claude Code Skill in the repo — drop it into &lt;code&gt;~/.claude/skills/&lt;/code&gt; and saying "export my Youdao notes" triggers the whole pipeline without remembering flags.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Came Out
&lt;/h2&gt;

&lt;p&gt;The final export: 2,285 notes across 253 folders. Mixed Chinese, English, and code content with no encoding issues. Tables, code blocks, images, lists, and blockquotes all converted correctly from the JSON block tree. Some notes were empty bodies with attachments only, handled gracefully.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;~/Desktop/obsidian&lt;/code&gt; as an Obsidian vault and everything is there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Worked
&lt;/h2&gt;

&lt;p&gt;The most interesting part of this project was not the parser or the block converter.&lt;/p&gt;

&lt;p&gt;It was the realization that a closed-format application with no export feature had been storing all my data in plain, unencrypted SQLite files, with a documented-enough block structure that could be reverse-engineered in an afternoon.&lt;/p&gt;

&lt;p&gt;Two thousand notes. Years of writing. The application offered no way to take them out. They were never locked in. I just hadn't looked.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/chncaesar/youdao-to-obsidian" rel="noopener noreferrer"&gt;github.com/chncaesar/youdao-to-obsidian&lt;/a&gt;&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>database</category>
      <category>productivity</category>
      <category>software</category>
      <category>sql</category>
    </item>
    <item>
      <title>I Don't Understand Embedded. I Was Still the Only One Who Could Ship It.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:25:23 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/i-dont-understand-embedded-i-was-still-the-only-one-who-could-ship-it-24c2</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/i-dont-understand-embedded-i-was-still-the-only-one-who-could-ship-it-24c2</guid>
      <description>&lt;p&gt;The local test suite was green. Eleven of eleven. I told the agent we had test coverage, and I said commit and push. We shipped an SDK that had never once been compiled by the chip's real compiler.&lt;/p&gt;

&lt;p&gt;I write backend systems. I do not write firmware. The project generates C code for a microcontroller, and the generated code had to compile inside CDK — the vendor's build environment — on a Windows machine, against a RISC-V cross-compiler, inside a packaging system I had never used. None of that existed on my Linux box. The eleven passing tests ran under host gcc — the compiler that builds programs for the machine they run on. The code that mattered needed a different compiler, for a different chip, in a place I couldn't reach. Two compilers, two worlds. My tests lived in the wrong one.&lt;/p&gt;

&lt;p&gt;A day later the first error came back from the Windows machine. Then another. Then five more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seven shots
&lt;/h2&gt;

&lt;p&gt;The errors arrived one at a time, and the agent answered each the same way: a confident "root cause:" and a fix pushed to the repo. A header file was missing. A type name was wrong — bugs no real compiler had ever touched. Then the SDK wasn't being compiled at all: an obscure manifest setting had been left out. Then the chip's pin names were wrong — the same pin called one thing in the SDK the agent wrote against, and another thing in the SDK CDK actually shipped. Then an entire driver file silently compiled to nothing, because the switch that turned it on lived in a generated file the compiler couldn't see. Then the same wall again, because the last fix had patched a symptom and the real problem was the architecture.&lt;/p&gt;

&lt;p&gt;Seven rounds. Each one the agent called the root cause. Each one was real, and each one was only the outermost skin of the next.&lt;/p&gt;

&lt;p&gt;What I want to point at is not the bugs. It is the shape of the loop. The agent fired a fix in about three seconds. Verifying it cost me minutes to hours: pull the code on Windows, run the full CDK build, read the error output, copy it back. The actual feedback loop, the one that touched ground truth, ran at human speed across two machines that could not talk to each other, with me manually shuttling errors between them. I had built an elaborate multi-agent workflow, and the real oracle in it was a person doing copy-paste.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it couldn't stop
&lt;/h2&gt;

&lt;p&gt;A confident "root cause:" at every step is not a sign of convergence. It is a sign of nothing. The agent emitted the prose of certainty regardless of whether it was getting closer or flailing, and at one point the fix it proposed was to simply rip out the switch that had silenced the driver file, so the thing would compile — a hack I shut down with a flat "that's too dirty." Only after that did the real fix surface: let the driver be chosen by which file you compile in, not by a switch buried in a file the compiler never sees. A structural change, not another patch.&lt;/p&gt;

&lt;p&gt;Here is the part worth sitting with. The agent never said the one true sentence: &lt;em&gt;I cannot see the target. Every fix I am giving you is a guess. Stop pulling — go install the RISC-V toolchain, and then I'll be useful.&lt;/em&gt; It never said it because each guess cost the agent three seconds and cost me an afternoon, and the agent does not pay that bill. An agent that doesn't bear the cost of a wrong guess will always prefer firing one more shot over asking for the instrument that would let it aim. The economics are silent, and they run entirely against the person at the keyboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  The briefing that wouldn't have helped
&lt;/h2&gt;

&lt;p&gt;The obvious lesson is the disappointing one: I should have explained the new component's mechanics to the agent up front. Tell it how the build environment wants its manifest written, how it pulls components into a build, where it expects the pieces to sit. Brief the new role, wire it to the existing ones, and the seven rounds collapse to zero.&lt;/p&gt;

&lt;p&gt;Except briefing the agent wouldn't have helped. The agent was not ignorant of CDK — it knew the build environment well enough to scaffold a component from scratch, write the manifest, lay out the search paths. At one point it explained, fluently and at length, how CDK resolves dependencies. The explanation was wrong, but it was not uninformed. The trap is exactly this: general knowledge of CDK is not the same as knowing how this version, wired to this version of the SDK, would actually behave when the build ran. The setting that had been left out, the manifest format, the pin names — those were facts about the runtime behavior of a target neither of us could observe. Its real knowledge ran right up to the edge of the part that bites, with no seam to mark where one ended and the other began.&lt;/p&gt;

&lt;p&gt;Here is where an experienced embedded engineer would have done something the agent could not. The port layer was written against one copy of the chip's SDK; CDK shipped a different copy. To a veteran, "two sources of the same SDK" trips a reflex — same chip, two SDKs, the symbols will not match, go diff them now. It's not magic. Given the right prompt, a smart model can produce the same checklist. The gap is not capability, it's posture: an engineer's scar rewrites their default to suspicion; a model's default is confidence — it charges. Someone has to steer it. In that session nobody did, because neither of us knew there was a trap to steer toward. And the second reason is blunter, and it doesn't care how smart the model is: the second SDK wasn't in its context. It lived on the Windows machine. The agent never had both files. You cannot glance at a trap, or be prompted to look for one, that is sitting in another room.&lt;/p&gt;

&lt;p&gt;So the question stops being "how do I brief the agent" and becomes "what does anyone do when the ground truth is unknown to everyone and lives on a machine I'll never see."&lt;/p&gt;




&lt;h2&gt;
  
  
  The scout
&lt;/h2&gt;

&lt;p&gt;You don't translate the wall. You admit it's dark, and you send the cheapest possible probe to light it up once — before writing the design, before writing the code. The move I should have made on day one was to put the RISC-V compiler on my own machine and run a syntax-only pass over the generated code — compile it just far enough to surface errors, without building a real binary. Seconds, locally, instead of symbol mismatches arriving through someone's clipboard a week later. Not because it would tell me everything. Because it would drag a piece of that unreachable world into a place I could actually touch.&lt;/p&gt;

&lt;p&gt;This does not abolish trial-and-error. Trial-and-error is how you explore the genuinely unknown; there is no version of this work where you think hard enough up front to skip it. What you get to choose is where you hit the wall, whose time you spend hitting it, and whether you hit it once or seven times. A scout doesn't avoid the dark. A scout pays the smallest price to make a piece of it known, and does it before committing the expedition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Second act: what I actually built
&lt;/h2&gt;

&lt;p&gt;Diagnosis without a fix is just another complaint about AI. So after the firmware finally linked, I went back and changed the distance between the agent and the truth it couldn't reach. Three changes, and they sit at three very different levels — it's worth being honest about which ones actually closed the gap and which ones only narrowed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tear the wall down.&lt;/strong&gt; The strongest fix was to make the code generator emit the CDK project file itself — the list of source files, the search paths, the memory layout, all of it. Two of the seven errors came from a human hand-configuring CDK and missing something. The fix wasn't to verify that configuration more carefully. It was to delete the manual step entirely, so there was nothing left to misconfigure. This is the move I'd never made in any earlier post: not adding a layer of verification, but removing the thing that needed verifying. When you can make a stretch of unknown terrain simply cease to exist, that beats any amount of careful scouting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raise a telescope — and admit where it points.&lt;/strong&gt; I added a check that runs at generation time, doing a syntax-only pass over the core SDK code, wired into the test suite. It pulls the type-name and missing-value class of bug from "your clipboard, days later" to "a local test, seconds later." That is real. But it runs under host gcc — my machine's compiler — not the RISC-V one, and so the pin-name mismatches, the manifest setting, the switch the compiler couldn't see — none of those are catchable by it, because they live in a world host gcc can never enter. The telescope got closer. It is still not aimed at the mountain that actually matters. The probe that would close that boundary — the RISC-V compiler running the same syntax-only pass locally on every build — I still have not installed. I know exactly what it is. It is sitting on the ground next to me. That is the honest state of this system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draw a map for the next one.&lt;/strong&gt; I wrote the hard-won facts down as rules: the manifest format, the missing setting, a table of which pin names differ. This froze the intelligence the seven shots cost me. These rules are loaded at the start of every session — they do take effect. But unlike the other two fixes, which are automated and need no further human attention, written rules depend on maintenance: someone has to keep them accurate, scope them correctly, and phrase them precisely enough that the next agent doesn't misinterpret them. It's the most human-dependent fix of the three.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a harness actually is
&lt;/h2&gt;

&lt;p&gt;I used to think the missing step in AI-assisted work was verification — check the behavior, check the spec, check for the same bug elsewhere. That's still true, and I've written it before. This time taught me the step underneath it.&lt;/p&gt;

&lt;p&gt;An agent only thinks inside its context. That isn't a limitation you fix; it's the shape of the thing. It knew the build environment in general — enough to sound, and be, genuinely competent. What it could not know was how this specific target would behave when the build actually ran, and it could not feel the seam between the two. Its real knowledge ran right up to the edge of the part that bites. So it didn't say "I'm blind." It fired seven confident shots at a wall, and the confidence came from the knowledge that was real. The danger was never that it couldn't see. The danger was that it couldn't tell where its sight ended.&lt;/p&gt;

&lt;p&gt;A person has to stand at that boundary and say the wall is dark. The person doesn't need to understand what's behind it — I didn't, and I still don't. What the person does is the one thing the agent structurally won't: bear the cost of being blind, and spend it deliberately on making the dark known. That is what a harness is. Not a test suite. A harness is the work of shortening the distance between an agent and a ground truth it will never walk toward on its own — because it doesn't know the truth is out there, and it doesn't pay the price of not knowing.&lt;/p&gt;

&lt;p&gt;I can't write firmware. I couldn't brief the agent on a single thing about that chip. And I was still the only one who could ship it, because shipping it had nothing to do with understanding embedded development. It had to do with standing on the boundary, admitting it was dark, and dragging the far machine one step closer. When the agent writes more of the code than you can read, that is the job that's left. Not knowing the domain. Knowing which wall nobody has hit yet, and being willing to pay to hit it first.&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>harness</category>
    </item>
    <item>
      <title>Your Agent Checked Everything. It Was Still Wrong.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Mon, 22 Jun 2026 01:51:52 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/your-agent-checked-everything-it-was-still-wrong-18kd</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/your-agent-checked-everything-it-was-still-wrong-18kd</guid>
      <description>&lt;p&gt;I have been running a multi-agent development workflow for months now — one model writes the design, another generates the code, a third reviews the implementation, and I approve the result. It works well most of the time. But recently, three failures went through this pipeline undetected, and they share a pattern I had not been able to articulate until all three were on the table.&lt;/p&gt;

&lt;p&gt;They are not impressive bugs. The individual root causes are straightforward once you see them. What makes them worth writing about is what they reveal about the structure of this kind of failure — and what kind of verification could have caught them before they reached production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Case 1: The retry loop that never fired
&lt;/h2&gt;

&lt;p&gt;An ETL pipeline synced data from a third-party ERP API to a PostgreSQL warehouse. Every 5 to 15 minutes, it pulled incremental changes using a session-based authentication mechanism. The code had a standard retry loop: three attempts, exponential backoff, and a &lt;code&gt;resetLogin()&lt;/code&gt; call on failure to re-authenticate before the next attempt. It had worked for weeks.&lt;/p&gt;

&lt;p&gt;Then one afternoon, every sync job started failing with the same error: &lt;code&gt;COPY from stdin failed: expected N values, got 1&lt;/code&gt;. The pipeline reported the error and retried. And retried. And kept failing, for hours, until someone noticed.&lt;/p&gt;

&lt;p&gt;The retry loop existed in the code. &lt;code&gt;resetLogin()&lt;/code&gt; existed in the code. Neither had ever triggered. The reason took some investigation. When the ERP session expired after roughly 8.5 hours, the API did not return a 401 or a 403 or any HTTP error. It returned an HTTP 200 with a body that decoded to &lt;code&gt;[["error message"]]&lt;/code&gt; — a structurally valid JSON array containing one row with one column. The Go JSON decoder had no reason to produce an error. The retry condition checked &lt;code&gt;if err != nil&lt;/code&gt;, found nothing, and moved on with what it believed was a single-row result set.&lt;/p&gt;

&lt;p&gt;The fix was not to improve the retry logic. The fix was a background goroutine that proactively refreshes the session on a 4-hour ticker — a keepalive that renders the reactive recovery path irrelevant. The retry loop, the piece of the system that was supposed to handle this exact scenario, had been dead on arrival.&lt;/p&gt;




&lt;h2&gt;
  
  
  Case 2: The coordinates that never existed
&lt;/h2&gt;

&lt;p&gt;A code generator in a desktop tool produced C source files for an embedded microcontroller. Given a visual UI design, it emitted header files with widget macro definitions, a context file with event dispatch functions, and resource indices mapping image addresses in external flash memory. Developers compiled the output into their firmware, flashed the chip, and saw something strange: dynamic text widgets rendering at position (0, 0) regardless of where they had been placed in the design.&lt;/p&gt;

&lt;p&gt;The generated code compiled without warnings. The linker found all referenced symbols. No function returned an error. Everything looked complete. But the runtime widget table — a separate generated file that maps each widget's ID to its x, y, width, and height — had never been written. Nobody had called the method that produced it. The header file declared the widget struct type with x/y fields. The resource manifest carried coordinate information. The design document described the widget table format. The code generator simply never emitted it.&lt;/p&gt;

&lt;p&gt;The generated output was reviewed for content correctness. The reviewer confirmed that the header macros pointed to the right resource addresses, that the event routing was correct, that the page definitions matched the design. All of that was true. Nobody asked: are we generating all the files we need to generate?&lt;/p&gt;




&lt;h2&gt;
  
  
  Case 3: Read the instructions, not the comment
&lt;/h2&gt;

&lt;p&gt;I was integrating an SDK for a new microcontroller. The vendor had provided a zip file with the chip's SDK, and the directory structure looked right: startup file, linker scripts for the specific chip model, peripheral drivers for SPI, DMA, GPIO — everything I expected.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;startup.S&lt;/code&gt; file opened with a comment header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;; GCC for CSKY Embedded Processors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSKY is a different embedded CPU architecture, not the one this chip used. The datasheet clearly stated the chip was RISC-V. But the SDK directory was named for this specific chip family, the subdirectories matched, and the startup file was sitting right where it belonged. The comment said one thing. The directory name said another. Which was correct?&lt;/p&gt;

&lt;p&gt;Two facts resolved it. First, the vendor had accidentally delivered the SDK for a different chip in the same product family — one that used CSKY. The directory name was misleading. Second, when the correct SDK arrived, its &lt;code&gt;startup.S&lt;/code&gt; had the same CSKY comment in the header, because the vendor had copied the template from a third-party toolchain and never cleaned it up. The actual instructions told a different story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;csrw    mtvec, a0        ; RISC-V: write machine trap vector
la      sp, g_top_irqstack
jal     main             ; RISC-V: jump-and-link to main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simplest verification was a grep: &lt;code&gt;grep -c "csrw\|mret" startup.S&lt;/code&gt; returned 3. It was RISC-V. The comment was a fossil.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not an Intelligence Problem
&lt;/h2&gt;

&lt;p&gt;All three agents in my workflow — the designer, the implementer, the reviewer — did something that, in isolation, was correct. The designer wrote a spec that matched the task. The implementer produced code that matched the spec. The reviewer confirmed the match. Every verification step passed. The system was consistent. It was consistently wrong.&lt;/p&gt;

&lt;p&gt;This is not an intelligence problem. It is a boundary problem. The agent does exactly what it is asked to do, within the context it is given. No one told it where the context ends.&lt;/p&gt;

&lt;p&gt;In Case 1, the context was "check if the HTTP call returned an error." No one said "also check whether the successful response actually contains data."&lt;/p&gt;

&lt;p&gt;In Case 2, the context was "check if the generated files are correct." No one said "also check whether all required output files exist."&lt;/p&gt;

&lt;p&gt;In Case 3, the context was "trust the SDK directory structure and documentation to be correct." No one said "verify the architecture claim against the actual instruction stream."&lt;/p&gt;

&lt;p&gt;In every case, the agent performed a complete verification of what it understood to be the scope of the task. The scope was wrong. And that gap — between what the agent checks and what actually needs to be true — is the failure mode that an AI-assisted workflow is structurally blind to.&lt;/p&gt;

&lt;p&gt;This is not a bug in the agent. It is a missing layer in the workflow. The agent will verify the things you tell it to verify. It will not independently discover new categories of things to verify. That discovery is still engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  What belongs on the checklist
&lt;/h2&gt;

&lt;p&gt;Here is what they look like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Semantic output validation: check what the success means, not just that it returned success.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An API call, a function return, a system call — each has a success path and a failure path. But some systems encode failure inside success. After every external call, ask: does the returned data look like data, or does it look like an error in disguise? Check the row count. Check the structure. Scan for error keywords. If the caller received 1 row when the expected number of columns is much larger, the data is already suspect regardless of what &lt;code&gt;err&lt;/code&gt; says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Output completeness: verify that all required artifacts exist, not just that the ones that exist are correct.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Code generation produces multiple files. Some are visible in the output directory. Some should be but are not. Before accepting a generation result, enumerate the expected output files and confirm each one exists as a non-empty file. Do not accept "the generated files look correct" as a substitute for "all generated files are present."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ground-truth verification: for any technical claim that can be tested by a machine, test it by a machine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Comments can lie. File names can lie. Directory structures can lie. The CPU architecture of a &lt;code&gt;.S&lt;/code&gt; file is not whatever the header comment says — it is whatever instructions the assembler would emit. The format of a binary file is not whatever the README claims — it is whatever the bytes at specific offsets contain. Before any agent workflow makes a design decision based on a technical assumption, verify that assumption with a command that reads the artifact directly. Do not pass claims forward unchecked.&lt;/p&gt;




&lt;p&gt;None of these three checkpoints is dramatic. Each is small enough to be added to a task definition or a review stage without substantially changing the workflow. Together, they cover the three failure modes I have now seen multiple times: undetected failure inside a successful response, missing output from a code generator, and a false claim repeated by a toolchain artifact.&lt;/p&gt;

&lt;p&gt;The agent is not going to ask itself "what should I also verify?" That question — what else could be wrong even when everything I checked is right — is not something an agent can generate from within its own context. It has to come from outside. It has to be designed. When you set up a multi-agent workflow, you are not just designing a pipeline for code. You are designing the boundaries of what each agent will treat as ground truth. If you do not place verification checkpoints at those boundaries, no one will.&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>
    </item>
    <item>
      <title>AI Fixed The Bug. Then I Found Two More Just Like It.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:55:14 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/ai-fixed-the-bug-then-i-found-two-more-just-like-it-1e5b</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/ai-fixed-the-bug-then-i-found-two-more-just-like-it-1e5b</guid>
      <description>&lt;p&gt;I reported a bug. AI fixed it. Three days later, the same bug appeared somewhere else. I reported it again, AI fixed it again, and two weeks after that a third instance surfaced — same root cause, different location, different symptom. At some point I realized the problem was not that AI kept making the mistake. The problem was that I kept treating each occurrence as a separate incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bug
&lt;/h2&gt;

&lt;p&gt;Early in the project I had established a rule: all monetary values must be stored as integers representing cents. Never floating point, never doubles, always integers. The rule existed because floating-point arithmetic on money produces invisible precision errors that accumulate silently over time. It was documented. The codebase had utility functions for converting between display values and stored integers.&lt;/p&gt;

&lt;p&gt;Despite all of this, the same mistake appeared three times across different parts of the codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// first occurrence&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// second occurrence, different feature&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priceInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// third occurrence, different feature again&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiResponse&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each time, AI had been asked to implement a new feature. Each time, it reached for &lt;code&gt;double.tryParse&lt;/code&gt; because that is the natural way to parse a number from text input — it's what most Dart code does, and it's what the training data overwhelmingly shows. Each time, a code review caught the specific instance that was reported. And each time, nobody checked whether the same pattern existed elsewhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  How AI Fixes Bugs
&lt;/h2&gt;

&lt;p&gt;When you show AI a bug, it focuses on that bug. It reads the surrounding code, understands the problem, and proposes a fix — and the fix is usually correct for the code it looked at. What it does not do is search the rest of the project for other places where the same mistake might exist. This is not a failure of intelligence. It is a failure of scope. AI responds to what you show it, and it does not independently decide to audit your entire codebase for related patterns.&lt;/p&gt;

&lt;p&gt;The difference becomes clear when you compare it to how an experienced engineer handles the same situation. Someone who has worked in a codebase for months develops an instinct for where the same patterns get repeated. When they fix a bug, they often think: I've seen this structure elsewhere, let me check those places too. That instinct comes from having written and read most of the code themselves. AI does not have this map. Every conversation starts fresh, and even if you have fixed the same mistake twice before in previous sessions, AI has no memory of having seen the pattern in your codebase. It does not know to be suspicious.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Broader Pattern
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;double.tryParse&lt;/code&gt; issue was one example, but the same dynamic played out several more times. After fixing a race condition in one part of the UI, I found two more places with the same structure. After fixing an error message that was leaking internal exception details to users, I found four more places doing the same thing. After fixing a database write operation that was missing an invalidation call before navigation, I found five more. Each time, AI fixed the reported instance correctly. Each time, the other instances were waiting quietly.&lt;/p&gt;

&lt;p&gt;The reason this happens more with AI than it did when I wrote most of the code myself is straightforward. When I introduce a bug, I at least know roughly where that pattern appears, because I wrote it. With AI generating large blocks of implementation that I review but don't write line by line, the mental map is thinner. And AI will consistently apply the same habits across multiple features, because it is generating from the same set of assumptions and defaults — which means when it has a bad habit, that habit is likely distributed across every place it was asked to do something similar.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;After the third occurrence of the &lt;code&gt;double.tryParse&lt;/code&gt; bug, I changed how I approach AI-assisted fixes. Now, before I show AI the specific fix, I do one step first: search the entire codebase for the pattern.&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"double.tryParse"&lt;/span&gt; lib/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"double.parse"&lt;/span&gt; lib/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I make a list of every occurrence, then fix all of them in one pass. This sounds obvious, but in practice it is easy to skip when AI is handling the fix and everything feels handled. The reported instance gets resolved, the review looks clean, and the invisible copies wait for their turn to surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Role That Has Not Changed
&lt;/h2&gt;

&lt;p&gt;Code generation is increasingly handled by AI. Code review is increasingly assisted by AI. Architecture can increasingly be drafted by AI. But the role that seems to be not just surviving but becoming more important is the engineer who holds the global view — who knows what patterns are repeated and where, who knows what invariants must hold across the entire system, and who, when a bug appears in one place, asks whether the same mistake exists somewhere else.&lt;/p&gt;

&lt;p&gt;AI does not have that view. It cannot. Every session starts fresh, and every conversation focuses on what was shown. The global view of your codebase lives in your head, and when AI is writing code faster than you can track, that global view is exactly what you need to protect. When you ask AI to fix a bug, the fix will likely be correct for the code you showed it. Before you accept that fix, take a minute and search for the same pattern. That question — where else could this be? — used to be instinctive. With AI writing more of the code, it needs to become deliberate.&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>harness</category>
    </item>
    <item>
      <title>The Design Doc Was Wrong. AI Trusted It Anyway.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:54:18 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/the-design-doc-was-wrong-ai-trusted-it-anyway-4309</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/the-design-doc-was-wrong-ai-trusted-it-anyway-4309</guid>
      <description>&lt;p&gt;I was building a new module for a side project, and the design document looked thorough. Claude had written it, I had reviewed it, and everything seemed reasonable. One section described how to calculate a derived value from two stored fields — both multiplied by 100 to preserve two decimal places — and gave the formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Result = fieldA × 100 × fieldB × 100 / 10000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logic made sense on the surface. Both fields carried 100x precision, so the divisor cancels them out. DeepSeek implemented it. The review agent verified it. I approved it. Then I opened the simulator and saw a value that should have been 1,200 displayed as 120,000.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Formula Was Wrong By A Factor Of 100
&lt;/h2&gt;

&lt;p&gt;The correct divisor was 1,000,000, not 10,000. When you multiply two fields that are each inflated by 100x, the product is inflated by 10,000x — so you need to divide by 1,000,000 to get back to base units, not 10,000. The design document had gotten this wrong, and everything downstream had faithfully reproduced that mistake.&lt;/p&gt;

&lt;p&gt;That part was easy to fix once I saw it. What I kept thinking about afterward was the process that had failed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Systems Read The Same Wrong Formula
&lt;/h2&gt;

&lt;p&gt;A human engineer reading that formula would probably do something instinctive: substitute real numbers. Two quantities, each stored as integer × 100. Say fieldA = 10000 and fieldB = 1200. Multiply: 12,000,000. Divide by 10,000: 1,200. But the expected result should be 12. That doesn't add up. The error surfaces in about five seconds, not through formal verification, just through the habit of running a quick sanity check before trusting a formula.&lt;/p&gt;

&lt;p&gt;None of the three systems in my workflow did this. DeepSeek read the design document and implemented the formula exactly as written. The review agent checked whether the implementation matched the design and confirmed that it did. I read both and approved them. Every statement was technically correct. The document was the source of truth, the code faithfully implemented the document, and the review verified the match. Nobody ran the numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Assumption I Had Been Making
&lt;/h2&gt;

&lt;p&gt;After I fixed the divisor, I started thinking about what had actually failed — and it wasn't the math. The math error was trivial once I looked at it carefully. What failed was a deeper assumption I had been making about how AI-assisted development works.&lt;/p&gt;

&lt;p&gt;I had been treating design documents as verified artifacts. The workflow in my head was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Design Document → AI Implementation → AI Review → Human Approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implicit assumption embedded in this workflow was that the design document itself was correct. If it wasn't, the whole chain would faithfully reproduce the error — which is exactly what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Documents Are Not Inputs. They Are Claims.
&lt;/h2&gt;

&lt;p&gt;A design document is not a verified fact. It is a claim made by whoever wrote it, and in this case that was Claude. Claude is very good at generating coherent, plausible-looking technical documentation. It is considerably less good at verifying whether the math embedded in that documentation is actually correct. I had been asking it to do both things at once — reason about architecture and verify arithmetic — without recognizing that it handles those two responsibilities very differently. It did the first well. It did the second poorly. And I hadn't thought to separate them.&lt;/p&gt;

&lt;p&gt;The fix I've settled on is simple: any formula in a design document is treated as unverified until someone has substituted concrete numbers and checked the result by hand. Not because AI gets formulas wrong frequently, but because when it does, the error propagates cleanly and invisibly through every downstream step. The formula is wrong, the implementation is wrong, the review confirms the implementation matches the formula, and everything is consistent. Everything is wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Code Did Exactly What It Was Supposed To Do
&lt;/h2&gt;

&lt;p&gt;Most bugs in AI-generated code are implementation bugs — a condition is inverted, a null check is missing, an edge case is unhandled. Those are failures where the code doesn't implement the intent correctly. This was different. The code implemented the intent perfectly. The intent was wrong. That category of error is much harder to catch with code review, because code review asks whether the code does what it's supposed to do. The answer here was yes. Nobody asked whether what it was supposed to do was actually correct.&lt;/p&gt;

&lt;p&gt;That question — is the specification right, not just the implementation — is increasingly where I think human judgment matters most in an AI-assisted workflow. AI is getting quite good at turning specifications into working code. It is getting reasonably good at catching implementation bugs. But it tends to treat the specification itself as ground truth, which means the work of questioning whether the spec makes sense in the first place is still entirely yours. AI will implement whatever you give it, with increasing reliability. The question is whether what you gave it was right.&lt;/p&gt;

&lt;p&gt;And right now, that part is still on you.&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>productivity</category>
    </item>
  </channel>
</rss>
