<?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: BBT Español</title>
    <description>The latest articles on DEV Community by BBT Español (bbt-es).</description>
    <link>https://dev.to/bbt-es</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%2Forganization%2Fprofile_image%2F15896%2Feeacdc1d-752e-4bf4-a95c-0b4114a7076e.png</url>
      <title>DEV Community: BBT Español</title>
      <link>https://dev.to/bbt-es</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bbt-es"/>
    <language>en</language>
    <item>
      <title>Cómo usar Claude Code como worker de osFoundry con tu propia suscripción de Claude</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:50:52 +0000</pubDate>
      <link>https://dev.to/bbt-es/como-usar-claude-code-como-worker-de-osfoundry-con-tu-propia-suscripcion-de-claude-k9l</link>
      <guid>https://dev.to/bbt-es/como-usar-claude-code-como-worker-de-osfoundry-con-tu-propia-suscripcion-de-claude-k9l</guid>
      <description>&lt;p&gt;Al terminar esta configuración, el rol Worker de &lt;a href="https://osfoundry.io/es" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; lo ocupa Claude Code. Cuando un paso del plan le llega, la app de escritorio arranca &lt;code&gt;claude&lt;/code&gt; en la carpeta de tu proyecto, con la sesión iniciada en tu propia suscripción de Claude. Claude Code edita archivos con sus propias herramientas y accede a las herramientas de osFoundry, como la búsqueda en tus notas, con el nombre &lt;code&gt;mcp__osfoundry__memory_search&lt;/code&gt;. Cada una de esas llamadas aparece en el chat de osFoundry como una línea "Using …".&lt;/p&gt;

&lt;h2&gt;
  
  
  Quién hace qué
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;App de escritorio de osFoundry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inicio de sesión&lt;/td&gt;
&lt;td&gt;Tu suscripción de Claude, que la CLI guarda después de &lt;code&gt;/login&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Nunca la ve. Quita &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; y variables similares del entorno del proceso hijo para que la ejecución no pueda pasar a facturación por API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coste&lt;/td&gt;
&lt;td&gt;Sale de tu suscripción&lt;/td&gt;
&lt;td&gt;Registra la ejecución con coste 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archivos&lt;/td&gt;
&lt;td&gt;Sus propios Read, Edit y Bash, dentro de una sola carpeta (&lt;code&gt;--add-dir&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Elige esa carpeta: la más reciente en Local Folders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Herramientas de osFoundry&lt;/td&gt;
&lt;td&gt;Las llama como &lt;code&gt;mcp__osfoundry__&amp;lt;tool&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Las sirve en un puerto loopback solo durante esta ejecución y comprueba cada llamada contra una allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aprobación&lt;/td&gt;
&lt;td&gt;Se ejecuta con &lt;code&gt;--permission-mode bypassPermissions&lt;/code&gt; en su carpeta&lt;/td&gt;
&lt;td&gt;Cada llamada que pasa por el bridge atraviesa la misma puerta de aprobación y políticas que la llamada a herramienta de cualquier otro modelo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Salida&lt;/td&gt;
&lt;td&gt;Texto final&lt;/td&gt;
&lt;td&gt;Se convierte en la salida del rol. La actividad de las herramientas se muestra como líneas de progreso&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Configuración
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Instala Claude Code e inicia sesión una vez
&lt;/h3&gt;

&lt;p&gt;Instala la CLI tal como lo documenta Anthropic. Después inicia sesión desde una terminal:&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="nv"&gt;$ &lt;/span&gt;claude
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;osFoundry busca el binario en &lt;code&gt;PATH&lt;/code&gt;, en &lt;code&gt;~/.claude/local&lt;/code&gt; y dentro del paquete de la extensión de VS Code (&lt;code&gt;~/.vscode/extensions/anthropic.claude-code-*/resources/native-binary/claude&lt;/code&gt;). Así que también encuentra un Claude Code que solo vino con la extensión de VS Code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Actívalo en External AI
&lt;/h3&gt;

&lt;p&gt;En la app de escritorio, haz clic en el icono de la llave de la cabecera (API Keys (BYOK)) y abre la pestaña External AI (IA externa). La fila de Claude Code debería verse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code   Installed                                  [Enabled]
/Users/you/.claude/local/claude
Model id: cli:claude (chat) · cli:claude:agent (autonomous)
[Verify sign-in]  [Sign in]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqzi2cklp0hde8t8elvk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqzi2cklp0hde8t8elvk.jpg" alt="Claude Code y Codex en la pestaña External AI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ejemplo: Claude Code y Codex en la pestaña External AI&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Haz clic en Verify sign-in (verificar inicio de sesión). La app ejecuta una completion mínima a través de la CLI y muestra ✓ Signed in o Needs sign-in. Sign in abre Claude Code en una ventana de terminal con la indicación "Type /login to sign in, then close this window."&lt;/p&gt;

&lt;p&gt;Puede que veas una insignia API key in env. Significa que hay una variable como &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; definida en el entorno desde el que se lanzó osFoundry. La app la elimina antes de arrancar &lt;code&gt;claude&lt;/code&gt;, así que la ejecución sigue usando tu suscripción. La insignia es solo un aviso.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Dale una carpeta
&lt;/h3&gt;

&lt;p&gt;El modo agente trabaja en un directorio real. Añade tu proyecto en Settings → Local Folders. Si tienes varios, se usa el último que añadiste. Las ejecuciones iniciadas desde la superficie Code usan en su lugar el repositorio registrado para esa ejecución.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Asigna el rol
&lt;/h3&gt;

&lt;p&gt;Abre la pestaña Pipeline → Pipeline Roles. En Agents, elige Worker y selecciona &lt;code&gt;cli:claude:agent&lt;/code&gt; en el grupo External AI (subscription).&lt;/p&gt;

&lt;p&gt;Los dos ids corresponden a dos modos distintos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cli:claude:agent&lt;/code&gt; es autónomo. Claude Code tiene sus propias herramientas en tu carpeta, más las herramientas de osFoundry a través del bridge. Úsalo para Worker, Researcher, Document Agent y roles parecidos.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cli:claude&lt;/code&gt; es el modo chat. La CLI se ejecuta con &lt;code&gt;--disallowedTools '*'&lt;/code&gt; y no puede llamar a nada. Sirve para roles solo de texto como Titling o Compaction. Si se asigna a un rol que necesita herramientas, la ejecución falla con un mensaje que te indica usar &lt;code&gt;cli:claude:agent&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los agentes que defines en la pantalla Agents también pueden usarlo. Su selector de modelo lo muestra bajo CLI agents, con la etiqueta "autonomous — runs in your local folder".&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Lo que la app arranca realmente
&lt;/h3&gt;

&lt;p&gt;En cada ejecución, osFoundry levanta un bridge MCP nuevo y luego lanza la CLI más o menos así. Los flags salen de &lt;code&gt;build_claude_command&lt;/code&gt; en &lt;code&gt;src-tauri/src/providers/agent_cli.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;conversation&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output-format&lt;/span&gt; stream-json &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="nt"&gt;--include-partial-messages&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; bypassPermissions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--add-dir&lt;/span&gt; /Users/you/work/site &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mcp-config&lt;/span&gt; &lt;span class="s1"&gt;'{"mcpServers":{"osfoundry":{"type":"http",
      "url":"http://127.0.0.1:&amp;lt;port&amp;gt;/mcp",
      "headers":{"Authorization":"Bearer &amp;lt;per-run token&amp;gt;"},
      "timeout":120000}}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--strict-mcp-config&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--append-system-prompt&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;role prompt&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--strict-mcp-config&lt;/code&gt; deja fuera de esta ejecución tus propios servidores MCP globales o de proyecto. Solo se conecta el servidor &lt;code&gt;osfoundry&lt;/code&gt;. El puerto y el token son nuevos en cada ejecución, y el listener se cierra cuando el proceso termina.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una ejecución real
&lt;/h2&gt;

&lt;p&gt;Ejemplo. La nota y el repositorio son contenido de demostración.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tú (chat de Maestro, Worker = cli:claude:agent)
  Busca en mis notas cuándo es la revisión del presupuesto del Q4 y añádelo a TODO.md.

Llamadas a herramientas de Claude Code
  mcp__osfoundry__memory_search  {"query": "revisión del presupuesto del Q4"}
    → "Revisión del presupuesto del Q4: jue 14:00."
  Read   TODO.md
  Edit   TODO.md   (+ "- [ ] Preparar las cifras para la revisión del presupuesto del Q4 (jue 14:00)")

Líneas de progreso de osFoundry
  Using memory_search — {"query":"revisión del presupuesto del Q4"}
  Using Read — {"file_path":"TODO.md"}
  Using Edit — {"file_path":"TODO.md", …}

Respuesta
  Añadido "Preparar las cifras para la revisión del presupuesto del Q4 (jue 14:00)" a TODO.md.
  La hora viene de tu nota "Revisión del presupuesto del Q4: jue 14:00."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El prefijo &lt;code&gt;mcp__osfoundry__&lt;/code&gt; es la forma en que Claude Code nombra las herramientas de un servidor llamado &lt;code&gt;osfoundry&lt;/code&gt;. osFoundry lo quita en la línea de progreso, así que &lt;code&gt;memory_search&lt;/code&gt; se lee igual que con un modelo por API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde está el límite
&lt;/h2&gt;

&lt;p&gt;El bridge ofrece una lista corta y fija. Incluye herramientas de solo lectura como &lt;code&gt;memory_search&lt;/code&gt;, &lt;code&gt;web_search&lt;/code&gt;, &lt;code&gt;extract_content&lt;/code&gt;, &lt;code&gt;code_search&lt;/code&gt;, &lt;code&gt;todo_list&lt;/code&gt;, &lt;code&gt;transcribe&lt;/code&gt; y las herramientas de lectura &lt;code&gt;cloud_*&lt;/code&gt;, además de la orquestación (&lt;code&gt;spawn_fleet&lt;/code&gt;, &lt;code&gt;graph_run&lt;/code&gt;, &lt;code&gt;loop_run&lt;/code&gt;), la pantalla en vivo y las herramientas de Canvas. Deja fuera a propósito &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;write_file&lt;/code&gt; y &lt;code&gt;execute_command&lt;/code&gt;. Claude Code ya tiene versiones de esas limitadas a su carpeta, y un segundo ejecutor sin ese límite por MCP llegaría más allá de &lt;code&gt;--add-dir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La allowlist la aplica el bridge, no un flag de la CLI. Una ejecución en modo &lt;code&gt;bypassPermissions&lt;/code&gt; no respeta &lt;code&gt;--allowedTools&lt;/code&gt;, así que osFoundry no lo pasa. Una llamada a cualquier herramienta fuera de la lista de la ejecución recibe un error JSON-RPC: "tool '…' is not available to this agent".&lt;/p&gt;

&lt;p&gt;El workspace y el canal quedan fijados cuando arranca el bridge. Una llamada a herramienta no puede redirigirse a otro workspace mediante sus argumentos.&lt;/p&gt;

&lt;p&gt;Las escrituras en la pantalla o en un editor de Canvas (&lt;code&gt;ui_agent_act&lt;/code&gt;, las herramientas de escritura de Canvas) siempre te preguntan antes, igual que con un modelo por API. Las ejecuciones que nadie está mirando, como un agente que responde a una mención en un canal, rechazan una herramienta que necesita aprobación en lugar de esperar un clic que no va a llegar.&lt;/p&gt;

&lt;p&gt;Tus archivos nunca pasan por osFoundry. Claude Code los lee y edita directamente, y su tráfico va a Anthropic con tu propio inicio de sesión, como iría desde una terminal.&lt;/p&gt;

&lt;p&gt;El siguiente paso es asignar &lt;code&gt;cli:claude:agent&lt;/code&gt; también al rol Researcher, junto a Worker, y lanzar una tarea orquestada. La &lt;a href="https://osfoundry.io/es/platform/pipeline-tab" rel="noopener noreferrer"&gt;página de roles del Pipeline&lt;/a&gt; enumera todos los roles que puedes darle, y &lt;a href="https://osfoundry.io/es/platform/local-folders" rel="noopener noreferrer"&gt;Local Folders&lt;/a&gt; explica cómo se elige la carpeta de trabajo.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>¿Cómo evita osFoundry que una edición de Claude sobrescriba la de una persona en la misma Note?</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:38:15 +0000</pubDate>
      <link>https://dev.to/bbt-es/como-evita-osfoundry-que-una-edicion-de-claude-sobrescriba-la-de-una-persona-en-la-misma-note-4cdc</link>
      <guid>https://dev.to/bbt-es/como-evita-osfoundry-que-una-edicion-de-claude-sobrescriba-la-de-una-persona-en-la-misma-note-4cdc</guid>
      <description>&lt;p&gt;Esta es la herramienta de escritura que recibe un chat de Claude para las Notes de &lt;a href="https://osfoundry.io/es" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt;, tal como la lista el endpoint MCP público. Se obtuvo con un &lt;code&gt;tools/list&lt;/code&gt; sin autenticar el 2026-11-06 y se recortó a los campos que importan aquí:&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_note_tab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Use an exact tab ID and version from read_note_tab. Use replace_text for Note/Markdown, slide_add or slides_add_outline for Slides, sheet_set_range for Sheet, or replace_tab_data for supported content tabs. File and Source attachments cannot be edited through this tool. A repeated idempotency key returns the first result."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&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;"required"&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;"note_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tab_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"expected_version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"idempotency_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;"operation"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"operation"&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="nl"&gt;"enum"&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;"replace_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace_tab_data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slide_add"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                            &lt;/span&gt;&lt;span class="s2"&gt;"slides_add_outline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sheet_set_range"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"sheet_id"&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="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;"string"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start_row"&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="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;"integer"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"start_col"&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="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;"integer"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"values"&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="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;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"items"&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="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;"array"&lt;/span&gt;&lt;span class="p"&gt;}}}},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&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="nl"&gt;"readOnlyHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destructiveHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"securitySchemes"&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="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;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&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;"osfoundry:write"&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;Dos de los cinco campos obligatorios no tienen nada que ver con la edición. &lt;code&gt;expected_version&lt;/code&gt; convierte la escritura en un compare-and-swap: solo se aplica si la Note sigue en la versión que leyó el agente. &lt;code&gt;idempotency_key&lt;/code&gt; hace que un reenvío no tenga efecto. Las otras escrituras de Notes, &lt;code&gt;create_note_tab&lt;/code&gt; y &lt;code&gt;add_note_source&lt;/code&gt;, exigen el mismo par. Las lecturas no: &lt;code&gt;list_note_tabs&lt;/code&gt;, &lt;code&gt;read_note_tab&lt;/code&gt;, &lt;code&gt;read_sheet_range&lt;/code&gt; (como máximo 100 por 100 celdas) y &lt;code&gt;list_note_attachments&lt;/code&gt; son todas &lt;code&gt;readOnlyHint: true&lt;/code&gt; con &lt;code&gt;osfoundry:read&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A continuación, lo que aporta ese contrato, medido sobre una reconstrucción. La comprobación de versión es solo la mitad. La otra mitad es lo que hace el agente cuando lo rechazan.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué rechazar una escritura obsoleta en lugar de fusionarla?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;replace_text&lt;/code&gt; envía el texto completo de una pestaña. Si Claude leyó la versión 7, una persona guardó la versión 8 y ahora Claude envía su texto, el servidor tiene dos documentos completos y le falta el ancestro común. No puede saber si Claude quería borrar el párrafo nuevo de la persona o si simplemente nunca lo vio. Con last-writer-wins se elige la respuesta equivocada sin que nadie se entere. Rechazar es la única respuesta que no adivina.&lt;/p&gt;

&lt;p&gt;Eso es concurrencia optimista. Nadie mantiene un bloqueo mientras un modelo piensa durante veinte segundos. Cada escritura indica en qué versión se basa, y el servidor solo hace el intercambio si esa versión sigue siendo la actual:&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="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 0 rows → conflict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La app de escritorio de osFoundry hace lo mismo desde el lado humano. En &lt;code&gt;apps/web/src/screens/notes/context/NotesContext.tsx&lt;/code&gt;, guardar una Note es un &lt;code&gt;PATCH /notes/:id&lt;/code&gt; que lleva &lt;code&gt;expected_updated_at&lt;/code&gt; (la última revisión del servidor que vio el editor) y, después de una escritura Connected, un &lt;code&gt;expected_version&lt;/code&gt; que &lt;code&gt;connected-note-save-guard.ts&lt;/code&gt; guarda en &lt;code&gt;localStorage&lt;/code&gt; para que una recarga no lo borre. Si el guardado falla, el editor conserva el contenido de la persona como borrador local y muestra un error de sincronización. Los dos editores escriben de forma condicional.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué debe hacer el agente cuando lo rechazan?
&lt;/h2&gt;

&lt;p&gt;Hay dos reacciones plausibles, y solo una funciona:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Volver a leer la pestaña, aplicar de nuevo el cambio previsto sobre el contenido actual y escribir con la versión nueva.&lt;/li&gt;
&lt;li&gt;Volver a leer solo la versión y reenviar el mismo payload.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La segunda pasa la comprobación y aun así sobrescribe a la persona, porque el payload se calculó a partir del texto antiguo. La comprobación no puede verlo. Una versión solo demuestra que has visto el número de versión más reciente, no que hayas usado su contenido.&lt;/p&gt;

&lt;h2&gt;
  
  
  La reconstrucción
&lt;/h2&gt;

&lt;p&gt;Aquí no se ejecuta ningún binario de Claude. &lt;code&gt;notes_store.py&lt;/code&gt; implementa &lt;code&gt;list_note_tabs&lt;/code&gt;, &lt;code&gt;read_note_tab&lt;/code&gt;, &lt;code&gt;read_sheet_range&lt;/code&gt;, &lt;code&gt;edit_note_tab&lt;/code&gt; (&lt;code&gt;replace_text&lt;/code&gt;, &lt;code&gt;sheet_set_range&lt;/code&gt;) y &lt;code&gt;create_note_tab&lt;/code&gt; sobre sqlite en modo WAL, con los mismos nombres de herramientas y de argumentos que en producción. Cada escritura se ejecuta dentro de &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt;: consulta la tabla de idempotencia, compara la versión, aplica el cambio, incrementa la versión y guarda el resultado, todo en una sola transacción. &lt;code&gt;race.py&lt;/code&gt; arranca dos hilos, cada uno con su propia conexión: un "Claude" guionizado y una persona guionizada. Cada uno hace leer → pensar → escribir, 100 veces. El tiempo de pensar está reducido: 20–80 ms para Claude y 5–40 ms para la persona, de modo que la ventana entre lectura y escritura del agente es la más larga, como con un modelo real. Cada escritura añade una línea única (&lt;code&gt;claude-17&lt;/code&gt;, &lt;code&gt;human-42&lt;/code&gt;), así que al final cada línea que falta es una actualización perdida que se puede contar. Python 3.9.6, Apple M1, semillas 1–5.&lt;/p&gt;

&lt;p&gt;La ruta de escritura, sin la fontanería de sqlite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_guarded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tab_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEGIN IMMEDIATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT result FROM idem WHERE key=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,)).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replayed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tab_version&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tab&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;note_version&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;expected_version&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version_conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;# replace_text / sheet_set_range / new tab
&lt;/span&gt;    &lt;span class="c1"&gt;# bump version, store (key, result), COMMIT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y el bucle del agente, con el interruptor de política:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edit_note_tab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tab_now&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_note_tab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reapply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;   &lt;span class="c1"&gt;# redo the change on what's there now
&lt;/span&gt;    &lt;span class="n"&gt;tab_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;                                      &lt;span class="c1"&gt;# refresh_version keeps the old text
&lt;/span&gt;    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;edit_note_tab&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un intento con conflicto recibe una idempotency key nueva. La clave identifica una escritura prevista sobre una base concreta. Tras un rechazo, la escritura que se vuelve a aplicar es otra escritura.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cuántas actualizaciones se pierden sin la comprobación?
&lt;/h2&gt;

&lt;p&gt;Cinco ejecuciones, 100 ediciones por editor en cada una, 1,000 líneas previstas en total por fila:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protección de escritura&lt;/th&gt;
&lt;th&gt;Agente ante un conflicto&lt;/th&gt;
&lt;th&gt;Líneas perdidas&lt;/th&gt;
&lt;th&gt;Perdidas: Claude / persona&lt;/th&gt;
&lt;th&gt;Conflictos: Claude / persona&lt;/th&gt;
&lt;th&gt;Máx. intentos&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ninguna&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;td&gt;125 / 175&lt;/td&gt;
&lt;td&gt;0 / 0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;versión de la Note&lt;/td&gt;
&lt;td&gt;relee y vuelve a aplicar&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 / 0&lt;/td&gt;
&lt;td&gt;219 / 133&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;versión de la Note&lt;/td&gt;
&lt;td&gt;relee la versión y reenvía el texto antiguo&lt;/td&gt;
&lt;td&gt;304&lt;/td&gt;
&lt;td&gt;117 / 187&lt;/td&gt;
&lt;td&gt;213 / 136&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sin comprobación desapareció el 30% de todas las ediciones, y la persona perdió más que Claude. Como el agente piensa más tiempo, sus escrituras se basan en lecturas más antiguas y sobrescriben más. Con la comprobación y volviendo a aplicar el cambio no se perdió nada. El 41–48% de las escrituras de Claude necesitaron un segundo intento, nunca un tercero. La media fue de 1.41–1.48 intentos para Claude y 1.24–1.30 para la persona. Todas las ejecuciones tardaron 9.6–10.5 s de tiempo real fuera cual fuera el modo, porque domina el tiempo de pensar.&lt;/p&gt;

&lt;p&gt;La tercera fila es la que hay que recordar. El servidor informó de más de 300 conflictos y no protegió nada. Una comprobación de versión obliga a volver a leer. No puede obligar a usar lo que se ha leído.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cuesta algo la comprobación?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;bench.py&lt;/code&gt; cronometra 2,000 llamadas secuenciales a &lt;code&gt;edit_note_tab&lt;/code&gt; con un &lt;code&gt;replace_text&lt;/code&gt; de 200 caracteres, tres repeticiones por modo. Mediana por escritura: 61.9–63.1 µs sin comprobación, 57.0–58.8 µs con la versión de la Note y 55.1–61.1 µs con una versión por pestaña. La comparación es una lectura indexada dentro de una transacción que ya está abierta de todos modos. La diferencia queda por debajo del ruido entre ejecuciones. El p99 (0.42–1.33 ms) es jitter del fsync de sqlite y no depende del modo.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué la versión es de la Note y qué cuesta eso?
&lt;/h2&gt;

&lt;p&gt;Las descripciones de las herramientas de osFoundry asocian una sola versión a toda la Note. &lt;code&gt;create_note_tab&lt;/code&gt; dice "Use the version from list_note_tabs", el listado a nivel de Note. En la ruta de la app de escritorio que aplica las ediciones Connected (&lt;code&gt;connected-note-application.ts&lt;/code&gt;), &lt;code&gt;expected_version&lt;/code&gt; se compara con el &lt;code&gt;updated_at&lt;/code&gt; de la Note. Así que una persona que escribe en la pestaña Sheet mueve la versión contra la que se comprueba la escritura de Claude en la pestaña de texto.&lt;/p&gt;

&lt;p&gt;En la reconstrucción eso se puede medir. Se ejecuta &lt;code&gt;race.py tabs&lt;/code&gt; con Claude editando la pestaña de texto y la persona escribiendo celdas en la pestaña Sheet, 3 ejecuciones, 100 ediciones cada una:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alcance de la versión&lt;/th&gt;
&lt;th&gt;Conflictos de Claude&lt;/th&gt;
&lt;th&gt;Conflictos de la persona&lt;/th&gt;
&lt;th&gt;Perdidas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Note&lt;/td&gt;
&lt;td&gt;130 de 300 escrituras (43%)&lt;/td&gt;
&lt;td&gt;87 de 300 (29%)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pestaña&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Los 217 conflictos son falsos, porque los dos editores nunca tocaron la misma pestaña. Cada uno cuesta una relectura y nada más. Una versión a nivel de Note da algo que una versión por pestaña no puede dar: cada escritura se comprueba contra todo lo que hay en la Note, así que un agente que leyó la Sheet para escribir el Brief no puede escribir un Brief basado en valores de la Sheet que han cambiado entretanto. Las versiones por pestaña eliminan los falsos conflictos y pierden justo esa garantía entre pestañas. Para un agente que lee una pestaña y escribe en otra, la versión gruesa es el valor por defecto más seguro.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué le hace un timestamp en milisegundos a una comprobación de versión?
&lt;/h2&gt;

&lt;p&gt;Una versión basada en timestamp solo funciona si dos guardados no pueden producir la misma cadena. &lt;code&gt;race.py fast&lt;/code&gt; baja el tiempo de pensar a cero, 2,000 ediciones por editor, 5 ejecuciones, y compara una versión truncada a milisegundos con un timestamp en microsegundos más un sufijo de 6 dígitos hexadecimales:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Formato de versión&lt;/th&gt;
&lt;th&gt;Líneas perdidas (de 20,000)&lt;/th&gt;
&lt;th&gt;Ejecuciones con pérdidas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;milisegundos&lt;/td&gt;
&lt;td&gt;136 (0.68%)&lt;/td&gt;
&lt;td&gt;5 de 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;microsegundos + sufijo&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 de 5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cuando dos escrituras hacen commit en el mismo milisegundo, la segunda recibe la misma versión que la primera. Un editor que leyó antes de la primera escritura pasa entonces la comprobación contra la segunda. osFoundry guarda las versiones de las Notes con la precisión completa de los timestamps de PostgreSQL. Si construyes lo tuyo, usa un contador o un timestamp con desempate.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué crear necesita su propia clave?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;create_note_tab&lt;/code&gt; no reemplaza nada, así que una comprobación de versión por sí sola no puede evitar duplicados. La llamada repetida es una pestaña nueva sobre la versión nueva. &lt;code&gt;race.py create&lt;/code&gt; crea 50 pestañas Sheet perdiendo el 30% de las respuestas, de modo que el cliente reintenta como haría un host tras un timeout:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Llamadas enviadas&lt;/th&gt;
&lt;th&gt;Pestañas creadas&lt;/th&gt;
&lt;th&gt;Duplicados&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sin clave, 3 ejecuciones&lt;/td&gt;
&lt;td&gt;71 / 61 / 63&lt;/td&gt;
&lt;td&gt;71 / 61 / 63&lt;/td&gt;
&lt;td&gt;21 / 11 / 13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;misma clave al reintentar&lt;/td&gt;
&lt;td&gt;71 / 61 / 63&lt;/td&gt;
&lt;td&gt;50 / 50 / 50&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Con la clave, el reintento devuelve el primer resultado, incluido el ID de su pestaña, así que el siguiente &lt;code&gt;edit_note_tab&lt;/code&gt; del agente apunta a la pestaña que existe. Es la línea "A repeated idempotency key returns the first tab" de la descripción de la herramienta en osFoundry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Si lo montas tú mismo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Exige &lt;code&gt;expected_version&lt;/code&gt; en todas las herramientas de escritura, en el esquema, no como campo opcional. Una comprobación opcional se salta la primera vez que un modelo va con prisa.&lt;/li&gt;
&lt;li&gt;Devuelve el conflicto como un resultado normal de la herramienta con el que el modelo pueda actuar, y di en la descripción de la herramienta qué hacer: volver a leer y rehacer el cambio. Tus instrucciones son el único sitio donde puede vivir la regla de "vuelve a aplicar, no te limites a actualizar la versión".&lt;/li&gt;
&lt;li&gt;Elige el alcance de la versión a propósito. Las versiones por documento dan falsos conflictos y protegen las lecturas entre pestañas. Las versiones por pestaña evitan los falsos conflictos y pierden esa protección.&lt;/li&gt;
&lt;li&gt;Haz que las versiones no puedan colisionar: un contador, o un timestamp con precisión completa y desempate. Nunca un timestamp redondeado a milisegundos.&lt;/li&gt;
&lt;li&gt;Protege las creaciones y los appends con una idempotency key que identifique una intención sobre una base. Guarda la clave y el resultado en la misma transacción que la escritura.&lt;/li&gt;
&lt;li&gt;Protege también el editor humano. Si solo se comprueban los conflictos del agente, una pestaña del navegador desactualizada puede seguir sobrescribiendo al agente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La definición breve de osFoundry del protocolo que hay debajo está en su &lt;a href="https://osfoundry.io/es/glossary/mcp" rel="noopener noreferrer"&gt;entrada del glosario sobre MCP&lt;/a&gt;. Cómo guarda y restaura estados anteriores de una Note se explica en la &lt;a href="https://osfoundry.io/es/platform/note-versions" rel="noopener noreferrer"&gt;página de versiones de Notes&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>claude</category>
      <category>concurrencia</category>
      <category>python</category>
    </item>
    <item>
      <title>Cómo hacer que Codex CLI lea tus notas y bases de conocimiento de osFoundry por MCP</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 14:57:37 +0000</pubDate>
      <link>https://dev.to/bbt-es/como-hacer-que-codex-cli-lea-tus-notas-y-bases-de-conocimiento-de-osfoundry-por-mcp-3b7e</link>
      <guid>https://dev.to/bbt-es/como-hacer-que-codex-cli-lea-tus-notas-y-bases-de-conocimiento-de-osfoundry-por-mcp-3b7e</guid>
      <description>&lt;p&gt;Al terminar, Codex CLI podrá consultar la Note de especificación, la base de conocimiento de soporte y los Records de bugs de uno de tus proyectos de &lt;a href="https://osfoundry.io/es" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; mientras trabaja en tu repositorio. Los lee a través del servidor MCP de osFoundry, te pregunta antes de cualquier cosa que escriba, y cada llamada que hace queda registrada en osFoundry en el Recent activity (actividad reciente) de la conexión. La configuración es una tabla en &lt;code&gt;~/.codex/config.toml&lt;/code&gt;, un inicio de sesión en el navegador y un clic para añadir la conexión a un proyecto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quién hace qué
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Lado de Codex&lt;/th&gt;
&lt;th&gt;Lado de osFoundry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inicio de sesión&lt;/td&gt;
&lt;td&gt;Tu inicio de sesión habitual de Codex, sin cambios. Codex guarda el token OAuth de osFoundry&lt;/td&gt;
&lt;td&gt;Tu cuenta de osFoundry aprueba la conexión una vez, para un workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quién ejecuta el modelo&lt;/td&gt;
&lt;td&gt;Codex, en tu máquina, con tu propio plan de Codex&lt;/td&gt;
&lt;td&gt;Nadie. En este camino no hay modelo ni clave de proveedor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dónde se ejecutan las herramientas&lt;/td&gt;
&lt;td&gt;Codex envía la llamada a la herramienta por HTTPS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;api.osfoundry.io&lt;/code&gt; la ejecuta y antes comprueba el acceso&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qué herramientas ve Codex&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;enabled_tools&lt;/code&gt; / &lt;code&gt;disabled_tools&lt;/code&gt; en &lt;code&gt;config.toml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;La checklist de Tools de la conexión&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A qué puede llegar&lt;/td&gt;
&lt;td&gt;A lo que devuelva el servidor&lt;/td&gt;
&lt;td&gt;Proyectos a los que añadiste la conexión, más los canales a los que la invitaste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aprobaciones&lt;/td&gt;
&lt;td&gt;El aviso propio de Codex, definido por &lt;code&gt;default_tools_approval_mode&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Los cambios en GitNest y Room esperan en "Needs your approval" (pendiente de tu aprobación)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dónde se quedan los datos&lt;/td&gt;
&lt;td&gt;Respuestas y diffs se quedan en tu sesión de Codex&lt;/td&gt;
&lt;td&gt;Notas, registros y artículos se quedan en osFoundry. Lo que es solo local nunca sale del dispositivo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Configuración
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Crea la conexión en osFoundry
&lt;/h3&gt;

&lt;p&gt;Haz clic en el icono de la llave en la cabecera de osFoundry y abre la pestaña Connected ("Connected agents", con el nombre del workspace actual debajo). En el formulario bajo "Connect in three steps":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assistant: Other MCP client&lt;/li&gt;
&lt;li&gt;Display name: algo que reconozcas, por ejemplo &lt;code&gt;Codex CLI&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Haz clic en Add assistant (añadir asistente)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La conexión aparece como "Setup pending. This assistant cannot read or change anything yet." Copia la Server address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.osfoundry.io/api/v1/external-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyvht1hpl3pi7zpgk2z0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyvht1hpl3pi7zpgk2z0.jpg" alt="La pestaña Connected con la conexión Codex CLI seleccionada" width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ejemplo: La pestaña Connected con la conexión Codex CLI seleccionada&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Añade el servidor a Codex
&lt;/h3&gt;

&lt;p&gt;El comando de una línea de la &lt;a href="https://learn.chatgpt.com/docs/extend/mcp?surface=cli" rel="noopener noreferrer"&gt;documentación de MCP de Codex&lt;/a&gt; de OpenAI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp add osfoundry &lt;span class="nt"&gt;--url&lt;/span&gt; https://api.osfoundry.io/api/v1/external-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso escribe una tabla &lt;code&gt;[mcp_servers.osfoundry]&lt;/code&gt;. Abre &lt;code&gt;~/.codex/config.toml&lt;/code&gt; y ajústala. Todas las claves de abajo están en esa documentación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mcp_servers&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;osfoundry&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"https://api.osfoundry.io/api/v1/external-mcp"&lt;/span&gt;
&lt;span class="n"&gt;scopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"osfoundry:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;default_tools_approval_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"writes"&lt;/span&gt;
&lt;span class="n"&gt;enabled_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"list_granted_resources"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"list_project_resources"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"list_note_tabs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"read_note_tab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"read_sheet_range"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"search_knowledge_base"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"read_knowledge_article"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"list_record_items"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"read_record_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"edit_record_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qué aporta cada línea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scopes&lt;/code&gt; sustituye lo que ofrece el servidor. Sin ella, Codex toma los scopes del challenge del servidor o de sus protected-resource metadata, y los metadatos de osFoundry enumeran cuatro, incluido &lt;code&gt;osfoundry:run&lt;/code&gt;. Un asistente de programación que solo lee y actualiza registros no necesita ejecutar sesiones.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;default_tools_approval_mode = "writes"&lt;/code&gt; hace que Codex pida confirmación para cada herramienta que no esté marcada como de solo lectura. osFoundry marca sus herramientas con &lt;code&gt;readOnlyHint&lt;/code&gt;, así que las nueve herramientas de lectura se ejecutan sin aviso y &lt;code&gt;edit_record_item&lt;/code&gt; pregunta primero.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enabled_tools&lt;/code&gt; mantiene corta la lista de herramientas. El servidor anuncia 59; esta lista tiene diez. Los nombres son los nombres MCP tal cual, sin prefijo. Un nombre mal escrito no da error. Simplemente la herramienta no aparece.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si el servidor solo debe aplicarse en un repositorio, pon la misma tabla en el &lt;code&gt;.codex/config.toml&lt;/code&gt; de ese repositorio. Codex solo lee la configuración de proyecto en los proyectos que hayas marcado como de confianza (trusted).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Inicia sesión
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codex mcp login osfoundry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El navegador abre &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;, con el título "Connect an agent". Muestra el Assistant client y "Returns to", que es una dirección local porque Codex recibe el callback en tu máquina. Después:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspace to connect: elige uno. Un segundo workspace necesita su propia conexión.&lt;/li&gt;
&lt;li&gt;Requested capabilities: con los &lt;code&gt;scopes&lt;/code&gt; de arriba verás dos líneas, "Read supported resources in projects you add" y "Change supported resources where you have edit access".&lt;/li&gt;
&lt;li&gt;Connection: elige "Use pending: Codex CLI" para que el inicio de sesión se vincule a la conexión del paso 1.&lt;/li&gt;
&lt;li&gt;Marca "I approve connecting this assistant to " y haz clic en Connect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En una máquina remota por SSH, &lt;code&gt;codex mcp login osfoundry --no-browser&lt;/code&gt; imprime la URL en su lugar. La abres en tu portátil y pegas de vuelta en la terminal la URL de redirección completa.&lt;/p&gt;

&lt;p&gt;El servidor de osFoundry tiene un endpoint de registro y acepta clientes públicos, así que Codex se registra solo. No tienes que pegar ningún client ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Añade la conexión a un proyecto
&lt;/h3&gt;

&lt;p&gt;De vuelta en icono de la llave → Connected, la conexión ahora dice "Connected." En Projects, elige el proyecto y haz clic en Add to project (añadir al proyecto). Eso abre las Notes sincronizadas del proyecto, sus bases de conocimiento, canales de Record, Rooms en la nube, repositorios GitNest asignados y su canal de discusión, incluido todo lo que se añada después. Los demás canales de Message necesitan un Invite (invitar) cada uno.&lt;/p&gt;

&lt;p&gt;La sección Tools empieza con "All supported tools are on by default." Desmarca lo que esta conexión no deba llamar nunca, aunque alguien edite &lt;code&gt;config.toml&lt;/code&gt; más adelante, y haz clic en Save tools (guardar herramientas).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fanbasoj837a8uv5zu2e6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fanbasoj837a8uv5zu2e6.jpg" alt="La checklist de Tools con solo las herramientas de lectura marcadas, y luego Save tools" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ejemplo: La checklist de Tools con solo las herramientas de lectura marcadas, y luego Save tools&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Compruébalo desde Codex
&lt;/h3&gt;

&lt;p&gt;En la TUI de &lt;code&gt;codex&lt;/code&gt;, &lt;code&gt;/mcp&lt;/code&gt; lista los servidores activos. &lt;code&gt;osfoundry&lt;/code&gt; debería estar ahí. Luego pide lo que necesites con palabras normales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una ejecución real
&lt;/h2&gt;

&lt;p&gt;Codex no está instalado en la máquina en la que se escribió esto, así que la sesión de abajo es un ejemplo simulado. Los nombres de las herramientas son los que el servidor real devuelve en &lt;code&gt;tools/list&lt;/code&gt;. El código fuente de Codex antepone &lt;code&gt;mcp__&amp;lt;server&amp;gt;__&lt;/code&gt; a las herramientas MCP en los nombres que ve el modelo, por eso aparecen así aquí. El prompt, el proyecto y las respuestas son contenido de demostración.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ejemplo (simulado)

&amp;gt; El test que falla en tests/test_refund.py espera una ventana de 14 días.
  Revisa la especificación de osFoundry y la KB de soporte antes de cambiar nada.

• mcp__osfoundry__list_granted_resources {}
  ← 1 proyecto: "Billing v2"
• mcp__osfoundry__list_project_resources {"project_id": "…", "resource_type": "note", "search": "reembolso"}
  ← Note "Reglas de reembolso (especificación)"
• mcp__osfoundry__list_note_tabs {"note_id": "…"}
  ← pestañas: Note (texto), Sheet "Casos límite"
• mcp__osfoundry__read_note_tab {"note_id": "…", "tab_id": "…"}
  ← "Los reembolsos se aceptan dentro de los 30 días siguientes a la fecha de la factura…"
• mcp__osfoundry__search_knowledge_base {"knowledge_base_id": "…", "query": "plazo de reembolso"}
  ← artículo "Política de reembolsos", actualizado hace 2 días: 30 días
• edited src/billing/refund.py, tests/test_refund.py
• mcp__osfoundry__edit_record_item {"record_channel_id": "…", "item_id": "…",
      "values": {"status": "Fixed"}, "expected_version": "…", "idempotency_key": "…"}
  [Codex pregunta: ¿permitir esta llamada a la herramienta? → sí]
  ← saved

El que estaba mal era el test, no el código. La Note de especificación y el
artículo de la KB dicen 30 días. Cambié el test y el comentario de la constante,
ejecuté pytest (12 aprobados) y puse el registro "Plazo de reembolso desfasado
16 días" en Fixed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;edit_record_item&lt;/code&gt; lleva la versión que devolvió &lt;code&gt;read_record_item&lt;/code&gt; o &lt;code&gt;list_record_items&lt;/code&gt;, así que un registro que alguien cambió entretanto se rechaza en lugar de sobrescribirse. Un &lt;code&gt;idempotency_key&lt;/code&gt; repetido devuelve el primer resultado, así que una llamada reintentada no escribe dos veces. El aviso de aprobación del medio viene de Codex, no de osFoundry, por &lt;code&gt;default_tools_approval_mode = "writes"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde está el límite
&lt;/h2&gt;

&lt;p&gt;Codex lee osFoundry; osFoundry no lee Codex. La conexión permite que Codex llame a herramientas cuando él lo decide. osFoundry registra cada llamada en Recent activity con el nombre de la herramienta, la decisión de acceso y la hora, pero no puede abrir tu sesión de Codex, tu repositorio ni tu terminal. Tu código fuente no va a ninguna parte salvo que Codex lo meta en un argumento de una herramienta, como un comentario en un registro.&lt;/p&gt;

&lt;p&gt;Los dos filtros hacen trabajos distintos. &lt;code&gt;enabled_tools&lt;/code&gt; en &lt;code&gt;config.toml&lt;/code&gt; decide qué se le ofrece al modelo, lo que mantiene el prompt corto y las opciones claras. La checklist de Tools en osFoundry es el interruptor del propietario y sigue vigente diga lo que diga el archivo local. Ninguno de los dos concede acceso a nada. Eso lo hace la pertenencia al proyecto.&lt;/p&gt;

&lt;p&gt;El acceso se decide en cada llamada. Cada llamada se comprueba contra el estado de la conexión, su workspace, la pertenencia actual al proyecto, el proyecto en el que está ahora el elemento y tu propio rol en él. Mueve una Note a otro proyecto, quita el proyecto o haz clic en Revoke connection (revocar conexión), y la siguiente llamada de Codex se rechaza aunque su token no haya caducado.&lt;/p&gt;

&lt;p&gt;Algunas escrituras son propuestas. El trabajo de GitNest, los change sets, las importaciones de archivos y los pull requests pasan por herramientas &lt;code&gt;propose_…&lt;/code&gt; y esperan en la pestaña Connected bajo "Needs your approval", con el destino y los archivos listados, hasta que hagas clic en Approve o Decline. Codex nunca recibe el token de tu proveedor de Git. De todos modos, la configuración de ejemplo de arriba deja fuera esas herramientas, ya que Codex ya tiene tu checkout local.&lt;/p&gt;

&lt;p&gt;Los datos solo locales se quedan en local. La respuesta &lt;code&gt;initialize&lt;/code&gt; del servidor lo dice: "Local-only data is unavailable to cloud clients." Los Rooms locales, las sesiones de Graph locales y todo lo que no se haya sincronizado no son visibles a través de esta conexión.&lt;/p&gt;

&lt;h2&gt;
  
  
  Siguiente paso
&lt;/h2&gt;

&lt;p&gt;Apunta Codex a una base de conocimiento que ya uses para responder preguntas y pídele que contraste con ella un docstring o un mensaje de error, por ejemplo: "¿El texto de la KB para este error coincide con lo que imprime el código?". &lt;code&gt;search_knowledge_base&lt;/code&gt; devuelve el ID del artículo y cuándo se actualizó por última vez, así que la fuente es fácil de verificar. Cómo las organiza osFoundry está en la &lt;a href="https://osfoundry.io/es/features/knowledge-bases" rel="noopener noreferrer"&gt;página de bases de conocimiento&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>codex</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cómo osFoundry encuentra tus sesiones de Claude Code y Codex en disco, recreado en Python</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 14:41:18 +0000</pubDate>
      <link>https://dev.to/bbt-es/como-osfoundry-encuentra-tus-sesiones-de-claude-code-y-codex-en-disco-recreado-en-python-17in</link>
      <guid>https://dev.to/bbt-es/como-osfoundry-encuentra-tus-sesiones-de-claude-code-y-codex-en-disco-recreado-en-python-17in</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/projects/-Users-alex-code-shop-api/c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58.jsonl
~/.claude/projects/-Users-alex-code-shop-api/c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58/subagents/agent-&amp;lt;id&amp;gt;.jsonl
~/.codex/state_5.sqlite                      -&amp;gt; table threads (id, rollout_path, cwd, git_branch, title, …)
~/.codex/sessions/2026/11/04/rollout-2026-11-04T10-00-00-&amp;lt;thread id&amp;gt;.jsonl

$ claude --resume c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58
$ codex resume 019a2f1c-7d0e-7b31-9c4e-5a1f0e2d8b63
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese es todo el contrato entre las CLI y un selector de sesiones. Claude Code y Codex guardan cada uno sus sesiones en archivos normales dentro de tu carpeta personal, y cada uno acepta un id para reanudar una. La pantalla External agents de la app de escritorio de &lt;a href="https://osfoundry.io/es" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; es un lector montado sobre esos archivos (Rust, &lt;code&gt;runtime/session_inventory.rs&lt;/code&gt; y &lt;code&gt;runtime/external_sessions.rs&lt;/code&gt;). Lista sesiones de doce apps, busca por título, carpeta, rama, app e id, y lanza en una terminal el comando de reanudación correcto.&lt;/p&gt;

&lt;p&gt;A continuación tienes la parte de Claude Code y Codex, recreada como un script de Python de 150 líneas y ejecutada contra las carpetas de sesiones reales del Mac donde se escribió este artículo. En ningún punto del artículo se imprime contenido de las conversaciones. Cada número es un recuento, un tamaño o un tiempo.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cómo se ve una sesión de Claude Code en disco?
&lt;/h2&gt;

&lt;p&gt;Un archivo JSONL por sesión, con el id de la sesión como nombre, dentro de una carpeta que lleva el nombre del directorio de trabajo. El nombre de la carpeta es la ruta absoluta con cada carácter que no sea letra o dígito sustituido por &lt;code&gt;-&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^A-Za-z0-9]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# /Users/alex/code/shop-api -&amp;gt; -Users-alex-code-shop-api
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comprobado con las 40 transcripciones de esta máquina: 40 de 40 carpetas siguen esa regla. La regla pierde información (&lt;code&gt;my.app&lt;/code&gt; y &lt;code&gt;my-app&lt;/code&gt; chocan, igual que &lt;code&gt;/&lt;/code&gt; y &lt;code&gt;.&lt;/code&gt;), así que nunca decodifiques el nombre de la carpeta. Lee &lt;code&gt;cwd&lt;/code&gt; de los registros.&lt;/p&gt;

&lt;p&gt;Los subagentes (Explore y compañía, de la herramienta Task) escriben sus propias transcripciones en &lt;code&gt;&amp;lt;session id&amp;gt;/subagents/agent-&amp;lt;id&amp;gt;.jsonl&lt;/code&gt;. Aquí había 129. Nadie las reanuda como sesión, y osFoundry se salta cualquier archivo cuyo nombre base empiece por &lt;code&gt;agent-&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cada línea es un registro con un &lt;code&gt;type&lt;/code&gt;. Contando los tipos en las primeras 400 líneas de cada uno de los 40 archivos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;th&gt;registros&lt;/th&gt;
&lt;th&gt;claves que importan para un índice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;assistant&lt;/td&gt;
&lt;td&gt;4,060&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;requestedModel&lt;/code&gt;, &lt;code&gt;effort&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt;, &lt;code&gt;message.model&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;attachment&lt;/td&gt;
&lt;td&gt;3,197&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user&lt;/td&gt;
&lt;td&gt;2,469&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt;, &lt;code&gt;permissionMode&lt;/code&gt;, &lt;code&gt;isSidechain&lt;/code&gt;, &lt;code&gt;message.content&lt;/code&gt;, &lt;code&gt;entrypoint&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;custom-title&lt;/td&gt;
&lt;td&gt;922&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customTitle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-name&lt;/td&gt;
&lt;td&gt;827&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agentName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;system, mode, queue-operation, file-history-&lt;em&gt;, api-request&lt;/em&gt;, …&lt;/td&gt;
&lt;td&gt;2,000+&lt;/td&gt;
&lt;td&gt;no hacen falta&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Los registros de conversación (&lt;code&gt;user&lt;/code&gt;, &lt;code&gt;assistant&lt;/code&gt;, &lt;code&gt;attachment&lt;/code&gt;, &lt;code&gt;system&lt;/code&gt;) llevan todos &lt;code&gt;sessionId&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt; y &lt;code&gt;version&lt;/code&gt;. &lt;code&gt;message.content&lt;/code&gt; es o un string o una lista de bloques; en esas líneas los tipos de bloque fueron &lt;code&gt;tool_result&lt;/code&gt; 2,261, &lt;code&gt;tool_use&lt;/code&gt; 2,223, &lt;code&gt;thinking&lt;/code&gt; 1,224 y &lt;code&gt;text&lt;/code&gt; 754. El título viene de un registro &lt;code&gt;custom-title&lt;/code&gt; o, en las versiones que lo escriben, de un registro &lt;code&gt;ai-title&lt;/code&gt; con &lt;code&gt;aiTitle&lt;/code&gt;. Aquí todos los archivos tenían &lt;code&gt;custom-title&lt;/code&gt; y ninguno &lt;code&gt;ai-title&lt;/code&gt;, así que lee los dos.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué no parsear cada archivo entero?
&lt;/h2&gt;

&lt;p&gt;Porque son grandes. Las 40 transcripciones de primer nivel suman aquí 19.4 GB. La mediana es de 168 MB, el más grande pesa 2.07 GB y el percentil 90 está en 1.94 GB.&lt;/p&gt;

&lt;p&gt;Pasar el archivo mediano por &lt;code&gt;json.loads&lt;/code&gt; línea a línea tardó 0.61 s (11,558 líneas, unos 279 MB/s en un Mac con Apple Silicon, con la caché de páginas caliente). A ese ritmo, parsear los 40 completos llevaría unos 70 s, antes de haber mostrado nada.&lt;/p&gt;

&lt;p&gt;Los datos que necesita una lista están en los dos extremos del archivo. Medido en los 40:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el primer registro con &lt;code&gt;cwd&lt;/code&gt; estaba como muy tarde en la línea 2 (mediana 2)&lt;/li&gt;
&lt;li&gt;el primer &lt;code&gt;gitBranch&lt;/code&gt; y el primer registro &lt;code&gt;user&lt;/code&gt;: como muy tarde en la línea 2&lt;/li&gt;
&lt;li&gt;había un registro de título dentro de los últimos 512 KiB en 40 de 40 archivos&lt;/li&gt;
&lt;li&gt;ninguna de las primeras 80 líneas pasaba de 256 KiB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Así que el índice lee las primeras 80 líneas y los últimos 512 KiB. Es lo mismo que hace osFoundry: &lt;code&gt;HEAD_LINES = 80&lt;/code&gt; con un límite de 256 KiB por línea en &lt;code&gt;session_inventory.rs&lt;/code&gt;, y &lt;code&gt;SETTINGS_TAIL = 512 * 1024&lt;/code&gt; en &lt;code&gt;external_sessions.rs&lt;/code&gt;, que se lee para sacar el último modelo, effort, modo de aprobación y rama. Leer cabeza y cola de los 40 archivos tardó 86 ms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;HEAD_LINES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LINE_CAP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TAIL_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEAD_LINES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LINE_CAP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LINE_CAP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# a pasted file or image: skip the rest of it
&lt;/span&gt;                &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nbytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TAIL_BYTES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;nbytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nbytes&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;   &lt;span class="c1"&gt;# first piece is a cut line
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;readline&lt;/code&gt; con límite es importante. Un turno de usuario con una imagen pegada es una sola línea de varios megas, y un simple &lt;code&gt;for line in f&lt;/code&gt; la cargaría entera en memoria solo para tirarla.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué campos necesita de verdad un selector?
&lt;/h2&gt;

&lt;p&gt;Cinco, más una fecha: app, id, carpeta, rama y título. La cabeza da &lt;code&gt;cwd&lt;/code&gt;, el primer &lt;code&gt;gitBranch&lt;/code&gt; y un título de reserva sacado del primer mensaje real del usuario (que no sea sidechain ni empiece por &lt;code&gt;&amp;lt;&lt;/code&gt;, que es como empiezan los envoltorios de comandos y hooks). La cola lo sobrescribe con el título y la rama más recientes, porque una sesión larga suele sobrevivir a la rama en la que empezó.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;claude_sessions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;proj&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;CLAUDE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;proj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;           &lt;span class="c1"&gt;# sub-agent transcript, not resumable
&lt;/span&gt;                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gitBranch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isSidechain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;first_text&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;                 &lt;span class="c1"&gt;# newest wins: titles, branch moves
&lt;/span&gt;                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customTitle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aiTitle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gitBranch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;records()&lt;/code&gt; es &lt;code&gt;json.loads&lt;/code&gt; por línea, saltándose las líneas rotas (la primera línea de una lectura de cola sale cortada por la mitad). &lt;code&gt;first_text()&lt;/code&gt; devuelve el contenido si es string, o el primer bloque &lt;code&gt;text&lt;/code&gt;. La fecha de último uso es el mtime del archivo, que también es lo que usa osFoundry para ordenar.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Dónde guarda Codex esos mismos datos?
&lt;/h2&gt;

&lt;p&gt;Codex escribe un rollout por sesión en un árbol por fechas, &lt;code&gt;~/.codex/sessions/YYYY/MM/DD/rollout-&amp;lt;timestamp&amp;gt;-&amp;lt;thread id&amp;gt;.jsonl&lt;/code&gt;, y guarda los archivados en &lt;code&gt;~/.codex/archived_sessions/&lt;/code&gt;. Aquí: 10 rollouts (28.5 MB) y 2 archivados. La primera línea de cada rollout es un registro &lt;code&gt;session_meta&lt;/code&gt; cuyo &lt;code&gt;payload&lt;/code&gt; tiene &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;originator&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;cli_version&lt;/code&gt;, &lt;code&gt;model_provider&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt; y, en algunas versiones, &lt;code&gt;git.branch&lt;/code&gt;. El resto son registros &lt;code&gt;event_msg&lt;/code&gt;, &lt;code&gt;response_item&lt;/code&gt; (con tipos de payload &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;function_call&lt;/code&gt;, &lt;code&gt;function_call_output&lt;/code&gt;, &lt;code&gt;reasoning&lt;/code&gt;, &lt;code&gt;custom_tool_call&lt;/code&gt;, …), &lt;code&gt;turn_context&lt;/code&gt; y &lt;code&gt;compacted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Codex además mantiene un índice, así que ni siquiera tienes que abrir los rollouts. &lt;code&gt;~/.codex/state_&amp;lt;n&amp;gt;.sqlite&lt;/code&gt; tiene una tabla &lt;code&gt;threads&lt;/code&gt;; aquí es &lt;code&gt;state_5.sqlite&lt;/code&gt;, con 43 columnas y 12 filas, entre ellas &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;rollout_path&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;git_branch&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;reasoning_effort&lt;/code&gt;, &lt;code&gt;approval_mode&lt;/code&gt;, &lt;code&gt;sandbox_policy&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;archived&lt;/code&gt; y &lt;code&gt;updated_at_ms&lt;/code&gt;. Puede haber más de un archivo &lt;code&gt;state_&amp;lt;n&amp;gt;&lt;/code&gt;; &lt;code&gt;codex_state_db&lt;/code&gt; en osFoundry coge el &lt;code&gt;n&lt;/code&gt; más alto, y el script hace lo mismo. Las filas cuyo &lt;code&gt;source&lt;/code&gt; es un objeto JSON son hilos de subagentes; sáltatelas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;codex_sessions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;codex_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?mode=ro&amp;amp;immutable=1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, cwd, git_branch, title, COALESCE(updated_at_ms, updated_at*1000), source &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FROM threads ORDER BY 5 DESC LIMIT 300&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;id_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;        &lt;span class="c1"&gt;# sub-agent thread
&lt;/span&gt;                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CODEX&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*/*/*/rollout-*.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;pl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Algo que costó un segundo intento: abrir la base de datos solo con &lt;code&gt;?mode=ro&lt;/code&gt; falló aquí con &lt;code&gt;unable to open database file&lt;/code&gt;. Añadir &lt;code&gt;immutable=1&lt;/code&gt; lo resolvió. Immutable le dice a SQLite que el archivo no va a cambiar mientras esté abierto, así que se salta el bloqueo y los archivos &lt;code&gt;-shm&lt;/code&gt;; para un índice que es una instantánea no pasa nada, y te permite leer una base de datos que la app tiene abierta. osFoundry abre el índice de Codex con &lt;code&gt;SQLITE_OPEN_READ_ONLY&lt;/code&gt;, y usa una apertura con &lt;code&gt;immutable=1&lt;/code&gt; para bases de datos que otra app puede tener abiertas en escritura.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué tan rápido va con carpetas reales?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 sessidx.py build          # four runs, same Mac, real ~/.claude and ~/.codex
52 sessions  claude 154 ms  codex 3 ms
52 sessions  claude 80 ms  codex 2 ms
52 sessions  claude 83 ms  codex 2 ms
52 sessions  claude 107 ms  codex 4 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;52 sesiones (40 de Claude Code, 12 de Codex), todas con carpeta y título, y las 40 de Claude Code con rama. La primera ejecución es la más fría; las demás fueron en caliente. El archivo de índice ocupa 11,480 bytes. Frente a un parseo completo de 70 s, 80 a 150 ms es la diferencia entre una lista que se abre al instante y otra con spinner.&lt;/p&gt;

&lt;p&gt;La búsqueda es entonces una comprobación de substring sobre cinco campos, los mismos que usa &lt;code&gt;matchesQuery&lt;/code&gt; de osFoundry en &lt;code&gt;lib/external-sessions.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sobre una carpeta personal falsa con la misma estructura (datos de ejemplo, no las sesiones reales):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ SESSIDX_HOME=./fx python3 sessidx.py find checkout
codex       019a2f1c  test/checkout-e2e    -
claude-code c5e1a7b2  fix/checkout-retry   Fix double charge on checkout retry
$ SESSIDX_HOME=./fx python3 sessidx.py resume c5e1
cd /Users/alex/code/shop-api &amp;amp;&amp;amp; claude --resume c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buscar dentro del texto de los mensajes significaría leer los 19.4 GB, o montar un índice de texto completo de todo lo que alguna vez pegaste en un agente. Buscar por carpeta y rama encuentra la sesión por el nombre del proyecto, que es lo que recuerdas de todas formas.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué añade una reanudación segura?
&lt;/h2&gt;

&lt;p&gt;Imprimir &lt;code&gt;claude --resume &amp;lt;id&amp;gt;&lt;/code&gt; es lo fácil. Lo que osFoundry añade alrededor (&lt;code&gt;plan_command&lt;/code&gt; y el plan de lanzamiento en &lt;code&gt;external_sessions.rs&lt;/code&gt;, más las comprobaciones que hace la pantalla antes de abrir la terminal) vale la pena copiarlo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ejecutar en la carpeta registrada, y negarse si ya no existe en lugar de reanudar en otro sitio.&lt;/li&gt;
&lt;li&gt;Comparar la rama registrada con &lt;code&gt;git -C &amp;lt;cwd&amp;gt; rev-parse --abbrev-ref HEAD&lt;/code&gt;. Si no coinciden, avisar y preguntar; nunca hacer checkout.&lt;/li&gt;
&lt;li&gt;Mantener el modelo (&lt;code&gt;--model&lt;/code&gt;, en Codex &lt;code&gt;-m&lt;/code&gt;) y el effort (&lt;code&gt;--effort&lt;/code&gt;, en Codex &lt;code&gt;-c model_reasoning_effort="…"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Mantener un modo de aprobación registrado solo si es al menos tan estricto como el predeterminado de la CLI. En Claude Code eso es &lt;code&gt;default&lt;/code&gt; o &lt;code&gt;plan&lt;/code&gt;; en Codex, aprobación &lt;code&gt;untrusted&lt;/code&gt; u &lt;code&gt;on-request&lt;/code&gt;, y sandbox &lt;code&gt;read-only&lt;/code&gt; o &lt;code&gt;workspace-write&lt;/code&gt;. Cualquier cosa más permisiva se retiene salvo que la persona lo active para ese lanzamiento concreto.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El &lt;code&gt;sessidx.py&lt;/code&gt; completo son las tres piezas de arriba más unas 30 líneas de pegamento (variables de entorno para cambiar las carpetas personales, salida JSON, y los subcomandos &lt;code&gt;find&lt;/code&gt; y &lt;code&gt;resume&lt;/code&gt;). En el Mac usado aquí no hay binario de claude ni de codex, así que la línea de reanudación se imprime, no se ejecuta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Si lo montas tú
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Trata los archivos como la API: solo lectura, nunca escribas junto a ellos y nunca abras los archivos de autenticación de las CLI para sacar un nombre de cuenta.&lt;/li&gt;
&lt;li&gt;Lee cabeza y cola, no transcripciones enteras. Limita la longitud de línea para que una imagen pegada no te cueste su tamaño en memoria.&lt;/li&gt;
&lt;li&gt;Saca &lt;code&gt;cwd&lt;/code&gt; de los registros, no del nombre de carpeta escapado.&lt;/li&gt;
&lt;li&gt;Usa preferentemente el &lt;code&gt;state_&amp;lt;n&amp;gt;.sqlite&lt;/code&gt; de Codex (el &lt;code&gt;n&lt;/code&gt; más alto) en vez de recorrer el árbol por fechas, y ábrelo como immutable si la app puede tenerlo abierto.&lt;/li&gt;
&lt;li&gt;Excluye de la lista las transcripciones de subagentes (&lt;code&gt;agent-*.jsonl&lt;/code&gt;, hilos con &lt;code&gt;source&lt;/code&gt; JSON); nadie las reanuda directamente.&lt;/li&gt;
&lt;li&gt;Decide de antemano qué puede cambiar una reanudación. Un valor seguro por defecto: carpeta sí, modelo sí, aprobaciones más permisivas solo si se piden, rama nunca.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La vista Sessions de los propios agentes de osFoundry trabaja con otra fuente; está descrita en la &lt;a href="https://osfoundry.io/es/platform/agent-sessions" rel="noopener noreferrer"&gt;página de sesiones de agentes de osFoundry&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>codex</category>
      <category>python</category>
      <category>sesiones</category>
    </item>
    <item>
      <title>Cómo conectar un chat de Claude a tus proyectos de osFoundry con un conector personalizado</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 13:42:37 +0000</pubDate>
      <link>https://dev.to/bbt-es/como-conectar-un-chat-de-claude-a-tus-proyectos-de-osfoundry-con-un-conector-personalizado-485a</link>
      <guid>https://dev.to/bbt-es/como-conectar-un-chat-de-claude-a-tus-proyectos-de-osfoundry-con-un-conector-personalizado-485a</guid>
      <description>&lt;p&gt;Al terminar, una conversación normal de Claude en claude.ai podrá abrir las Notes, las bases de conocimiento y los Records de uno de tus proyectos de &lt;a href="https://osfoundry.io/es" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt;, responder a partir de ellos y publicar en el canal de discusión de ese proyecto. Con tu aprobación, también podrá editarlos. Sigues con tu sesión de Claude como siempre. En cada llamada a una herramienta, osFoundry decide qué puede tocar ese chat, y cada llamada queda registrada en osFoundry en el Recent activity de la conexión. ChatGPT se conecta igual, con la misma dirección de servidor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quién hace qué
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Lado de Claude&lt;/th&gt;
&lt;th&gt;Lado de osFoundry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inicio de sesión&lt;/td&gt;
&lt;td&gt;Tu cuenta de Claude, sin cambios&lt;/td&gt;
&lt;td&gt;Tu cuenta de osFoundry aprueba la conexión una vez, para un workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qué ejecuta el modelo&lt;/td&gt;
&lt;td&gt;Claude, en el chat que ya usas&lt;/td&gt;
&lt;td&gt;Nada. En este camino no hay modelo ni clave de proveedor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dónde se ejecutan las herramientas&lt;/td&gt;
&lt;td&gt;Claude envía la llamada a la herramienta&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;api.osfoundry.io&lt;/code&gt; la ejecuta y antes comprueba el acceso&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qué puede alcanzar&lt;/td&gt;
&lt;td&gt;Lo que devuelva el servidor&lt;/td&gt;
&lt;td&gt;Los proyectos a los que añadiste la conexión, más los canales a los que la invitaste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aprobaciones&lt;/td&gt;
&lt;td&gt;El aviso de permisos de herramientas del propio Claude&lt;/td&gt;
&lt;td&gt;Los cambios en GitNest y Room esperan en "Needs your approval"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dónde se quedan los datos&lt;/td&gt;
&lt;td&gt;La respuesta vive en tu chat de Claude&lt;/td&gt;
&lt;td&gt;Las Notes, los records y los artículos se quedan en osFoundry. Lo que es solo local nunca sale del dispositivo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Configuración
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Crea la conexión en osFoundry
&lt;/h3&gt;

&lt;p&gt;En osFoundry, haz clic en el icono de la llave en la cabecera y abre la pestaña Connected. El panel se titula "Connected agents" y muestra en qué workspace estás. En "Connect in three steps":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assistant: elige Claude&lt;/li&gt;
&lt;li&gt;Display name: cualquier nombre que reconozcas después, por ejemplo &lt;code&gt;Claude research&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Haz clic en Add assistant (añadir asistente)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La nueva conexión muestra "Setup pending. This assistant cannot read or change anything yet." Copia la Server address con el botón Copy que tiene al lado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.osfoundry.io/api/v1/external-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhi61cfxig1v7yltczuxr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhi61cfxig1v7yltczuxr.jpg" alt="La pestaña Connected después de Add assistant, con la dirección del servidor y una conexión pendiente" width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ejemplo: La pestaña Connected después de Add assistant, con la dirección del servidor y una conexión pendiente&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Añádela a Claude como conector personalizado
&lt;/h3&gt;

&lt;p&gt;En Claude, abre Customize → Connectors y añade un conector personalizado con esa URL. En un plan Team o Enterprise, primero la añade un propietario de la organización en Organization settings → Connectors, y luego cada miembro se conecta. Los pasos de Anthropic para conectores MCP remotos están en la &lt;a href="https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp" rel="noopener noreferrer"&gt;guía de conectores personalizados de Claude&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Al hacer clic en Connect, Claude encuentra por sí solo el inicio de sesión de osFoundry. No pegas ningún client ID ni ninguna clave.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Aprueba un workspace
&lt;/h3&gt;

&lt;p&gt;El navegador llega a &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;, una página titulada "Connect an agent". Muestra el cliente del asistente y el host al que vuelve, y después pide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspace to connect: un solo workspace. Un segundo workspace necesita su propia conexión.&lt;/li&gt;
&lt;li&gt;Requested capabilities, en lenguaje claro: "Read supported resources in projects you add", "Change supported resources where you have edit access", "Run sessions where you grant a runner role".&lt;/li&gt;
&lt;li&gt;Connection: elige "Use pending: Claude research" para que se vincule a la conexión del paso 1, o deja "Create a new connection".&lt;/li&gt;
&lt;li&gt;Marca "I approve connecting this assistant to " y luego Connect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vuelves a Claude. La conexión está activa, pero todavía no ve nada. Una conexión nueva empieza con cero proyectos.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Añádela a un proyecto
&lt;/h3&gt;

&lt;p&gt;De vuelta en icono de la llave → Connected, selecciona la conexión. Ahora dice "Connected." En Projects, elige un proyecto y haz clic en Add to project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9myc1s54srpz8aum8m06.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9myc1s54srpz8aum8m06.jpg" alt="La conexión añadida a un proyecto, con su chat vinculado debajo" width="800" height="1110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ejemplo: La conexión añadida a un proyecto, con su chat vinculado debajo&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Esto da acceso al contenido sincronizado compatible de ese proyecto: Notes, bases de conocimiento, canales de Record, Rooms en la nube, los repositorios de GitNest asignados a él y el canal de discusión del proyecto. El contenido que añadas al proyecto más adelante también se incluye. Los canales de Message normales van aparte. Usa Message channels → Choose a Message channel → Invite para cada uno en el que el chat deba leer y responder.&lt;/p&gt;

&lt;p&gt;La sección Tools dice "All supported tools are on by default." Desmarca las que no quieras que tenga esta conexión y haz clic en Save tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tyzev2to95pu562is99.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tyzev2to95pu562is99.jpg" alt="La lista de Tools de una conexión de Claude, todas activadas por defecto" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ejemplo: La lista de Tools de una conexión de Claude, todas activadas por defecto&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Actívala en el chat
&lt;/h3&gt;

&lt;p&gt;En una conversación de Claude, abre el menú + → Connectors y activa el conector de osFoundry. Después, pide lo que necesites en lenguaje normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una ejecución real
&lt;/h2&gt;

&lt;p&gt;El prompt y la respuesta de abajo son contenido de ejemplo de un proyecto de demostración. Los nombres de las herramientas y de los argumentos son los que el servidor en producción anuncia en &lt;code&gt;tools/list&lt;/code&gt;. En claude.ai, las llamadas aparecen bajo el nombre del conector. Si añades la misma URL a Claude Code como &lt;code&gt;osfoundry&lt;/code&gt;, las mismas herramientas aparecen como &lt;code&gt;mcp__osfoundry__list_project_resources&lt;/code&gt;, etc.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ejemplo

Tú: Usando osFoundry, ¿qué sigue abierto en la checklist del lanzamiento Q3?
    Revisa la KB de soporte por si algo la contradice y luego marca
    el record "pricing page" como Ready.

Claude → list_granted_resources {}
       ← 1 proyecto conectado: "Launch Q3" (workspace)
Claude → list_project_resources {"project_id": "…", "resource_type": "note", "search": "checklist"}
       ← Note "Q3 launch checklist"
Claude → list_note_tabs {"note_id": "…"}
       ← pestañas: Note (texto), Sheet "Owners"
Claude → read_note_tab {"note_id": "…", "tab_id": "…"}
       ← 9 elementos, 3 sin marcar
Claude → search_knowledge_base {"knowledge_base_id": "…", "query": "refund window"}
       ← artículo "Refund policy" (actualizado hace 2 días)
Claude → read_record_item {"record_channel_id": "…", "item_id": "…"}
       ← "pricing page", status Draft, version …
Claude → edit_record_item {"record_channel_id": "…", "item_id": "…",
                           "values": {"status": "Ready"},
                           "expected_version": "…", "idempotency_key": "…"}
       [Claude te pide permitir esta llamada → Allow]
       ← guardado, nueva version …

Claude: Hay tres elementos abiertos: la fecha de embargo de prensa,
        la traducción de las FAQ y la línea de reembolsos de la
        pricing page. La checklist dice 14 días; el artículo de la KB
        "Refund policy" dice 30. He marcado el record de la pricing
        page como Ready.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dos detalles de esa transcripción son intencionados. &lt;code&gt;edit_record_item&lt;/code&gt; necesita la version que devolvió &lt;code&gt;read_record_item&lt;/code&gt;, así que un record que haya cambiado entretanto se rechaza en lugar de sobrescribirse. Un &lt;code&gt;idempotency_key&lt;/code&gt; repetido devuelve el primer resultado, así que una llamada reintentada no escribe dos veces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde está el límite
&lt;/h2&gt;

&lt;p&gt;osFoundry no puede abrir tus chats de Claude. La conexión MCP permite que Claude llame a herramientas de osFoundry cuando él lo decide. osFoundry no tiene ninguna API en la dirección contraria para listar, leer o iniciar una conversación personal. La herramienta que vincula un chat a un proyecto (&lt;code&gt;link_current_chat&lt;/code&gt;) recibe una etiqueta que el propio chat declara, y osFoundry la trata como una pista de enrutamiento, no como una identidad verificada. Por la misma razón, Claude no recibe notificaciones en segundo plano de osFoundry. Actúa cuando abres la conversación y le pides algo.&lt;/p&gt;

&lt;p&gt;Los datos solo locales se quedan en local. El propio servidor lo dice en su respuesta a &lt;code&gt;initialize&lt;/code&gt;: "Local-only data is unavailable to cloud clients." Las Rooms locales, las sesiones locales de Graph y el contenido que nunca se sincronizó no son visibles para la conexión. Compartir sesiones de Graph del escritorio es un interruptor aparte en la misma pestaña, y la app de escritorio tiene que estar abierta y con la sesión iniciada.&lt;/p&gt;

&lt;p&gt;El acceso se comprueba en cada llamada, no en cada inicio de sesión. Cada llamada se contrasta con el estado de la conexión, su workspace, la membresía actual del proyecto, en qué proyecto está el elemento en ese momento y tu propio rol sobre ese elemento. Mueve una Note a otro proyecto, quita el proyecto o haz clic en Revoke connection, y la siguiente llamada se rechaza. Las ediciones también requieren que tú, la persona que aprobó la conexión, sigas teniendo permisos de edición.&lt;/p&gt;

&lt;p&gt;Algunas escrituras se convierten en propuestas. El trabajo en GitNest, los change sets, las importaciones de archivos, los pull requests y las instalaciones de apps en Rooms pasan por herramientas &lt;code&gt;propose_…&lt;/code&gt;. Esperan en la pestaña Connected, en "Needs your approval", donde ves el repositorio o la Room de destino y los archivos antes de elegir Approve o Decline. Claude nunca recibe tu token del proveedor de Git ni ningún valor secreto.&lt;/p&gt;

&lt;p&gt;Los canales de Message necesitan invitación. Estar en un proyecto solo abre el canal de discusión de ese proyecto. Cualquier otro canal necesita un Invite explícito, y un canal privado necesita que lo invite su propietario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Siguiente paso
&lt;/h2&gt;

&lt;p&gt;Añade una base de conocimiento al mismo proyecto y hazle a Claude una pregunta que normalmente responderías con ella, por ejemplo "¿Qué dice nuestra KB sobre reembolsos, con el nombre del artículo?". &lt;code&gt;search_knowledge_base&lt;/code&gt; devuelve el ID del artículo y cuándo se actualizó por última vez, así que puedes comprobar la fuente. Cómo se organizan las bases de conocimiento de osFoundry se explica en la &lt;a href="https://osfoundry.io/es/features/knowledge-bases" rel="noopener noreferrer"&gt;página de bases de conocimiento&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>¿Cómo encuentra un conector personalizado de Claude el servidor OAuth de osFoundry y por qué se comprueba el acceso en cada llamada?</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Fri, 06 Nov 2026 10:30:52 +0000</pubDate>
      <link>https://dev.to/bbt-es/como-encuentra-un-conector-personalizado-de-claude-el-servidor-oauth-de-osfoundry-y-por-que-se-5ae7</link>
      <guid>https://dev.to/bbt-es/como-encuentra-un-conector-personalizado-de-claude-el-servidor-oauth-de-osfoundry-y-por-que-se-5ae7</guid>
      <description>&lt;p&gt;Esto es lo que recibe un cliente MCP remoto cuando llama a una herramienta de &lt;a href="https://osfoundry.io/es" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; sin token. La petición se hizo con curl desde un Mac en Tokio el 2026-11-06:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s -D - -X POST https://api.osfoundry.io/api/v1/external-mcp \
    -H 'content-type: application/json' \
    -H 'accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
         "params":{"name":"list_project_resources","arguments":{}}}'

HTTP/2 401
www-authenticate: Bearer resource_metadata="https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp", error="invalid_token", error_description="Connect osFoundry to continue"

{"jsonrpc":"2.0","id":3,"result":{"isError":true,
  "content":[{"type":"text","text":"Authentication required"}],
  "_meta":{"mcp/www_authenticate":["Bearer resource_metadata=\"https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp\", ..."]}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con esa única cabecera, el conector personalizado de Claude, o la conexión MCP del modo desarrollador de ChatGPT, encuentra la página de inicio de sesión. El usuario solo pega la URL. Todo lo que sigue sale de peticiones sin autenticar al endpoint en producción, más una pequeña reconstrucción en Python, ejecutada en local, que mide lo que un curl no puede enseñar: qué pasa con el acceso después de iniciar sesión.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué ve el cliente antes de tener un token?
&lt;/h2&gt;

&lt;p&gt;Tres llamadas, tres respuestas distintas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Petición&lt;/th&gt;
&lt;th&gt;Estado&lt;/th&gt;
&lt;th&gt;Qué devuelve&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initialize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;protocolo &lt;code&gt;2025-06-18&lt;/code&gt;, servidor &lt;code&gt;osfoundry 0.1.0&lt;/code&gt;, más instrucciones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;59 herramientas, 45,368 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/call&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WWW-Authenticate&lt;/code&gt; apuntando a los metadatos del recurso&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GET&lt;/code&gt; sobre el endpoint&lt;/td&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;no hay stream SSE por GET&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El handshake y el catálogo de herramientas son públicos. Solo llamar a una herramienta exige token. Así el host puede enseñar al usuario qué herramientas existen antes de que nadie inicie sesión. Nada del catálogo son datos de usuario, y el texto de &lt;code&gt;instructions&lt;/code&gt; en &lt;code&gt;initialize&lt;/code&gt; es genérico:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use list_granted_resources first. Every Note edit must use the exact tab ID.
Local-only data is unavailable to cloud clients.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada herramienta de &lt;code&gt;tools/list&lt;/code&gt; declara el scope OAuth que necesita y si solo lee:&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_record_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&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="nl"&gt;"readOnlyHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destructiveHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"openWorldHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"securitySchemes"&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="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;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&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;"osfoundry:write"&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;De las 59 herramientas, 30 necesitan &lt;code&gt;osfoundry:read&lt;/code&gt; y llevan &lt;code&gt;readOnlyHint: true&lt;/code&gt;, 26 necesitan &lt;code&gt;osfoundry:write&lt;/code&gt; y 3 necesitan &lt;code&gt;osfoundry:run&lt;/code&gt; (&lt;code&gt;request_session_run&lt;/code&gt;, &lt;code&gt;run_graph_session&lt;/code&gt;, &lt;code&gt;cancel_graph_run&lt;/code&gt;). Un host puede usar &lt;code&gt;readOnlyHint&lt;/code&gt; para decidir qué llamadas piden confirmación al usuario.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Adónde le manda el 401?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resource_metadata&lt;/code&gt; sigue la RFC 9728 (OAuth Protected Resource Metadata). El sufijo well-known va justo después del host, con la ruta del recurso añadida detrás. No se añade al final de la URL del recurso:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /.well-known/oauth-protected-resource/api/v1/external-mcp   200
GET /.well-known/oauth-protected-resource                       200 (same document)
GET /api/v1/external-mcp/.well-known/oauth-protected-resource   404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-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;"authorization_servers"&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;"https://api.osfoundry.io"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&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;"osfoundry:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:models"&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;Si escribes un cliente a mano, ese 404 es el primer error que vas a cometer. Lee la URL de la cabecera en lugar de construirla tú.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué dice de sí mismo el servidor de autorización?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt; (RFC 8414):&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="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"registration_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"grant_types_supported"&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;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refresh_token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint_auth_methods_supported"&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;"none"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"code_challenge_methods_supported"&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;"S256"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_response_iss_parameter_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Léelo como una cadena de decisiones. Hay un &lt;code&gt;registration_endpoint&lt;/code&gt;, así que el host puede registrarse solo (registro dinámico de clientes) y el usuario nunca toca un client ID. La autenticación en el token es &lt;code&gt;none&lt;/code&gt;, así que el host es un cliente público sin client secret, y lo que ata el código al cliente que lo pidió es PKCE con S256. &lt;code&gt;iss&lt;/code&gt; en la respuesta de autorización evita confusiones cuando un host habla con varios servidores de autorización.&lt;/p&gt;

&lt;p&gt;El paso de autorización lleva el navegador a la página de consentimiento de osFoundry, &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;. Ahí el usuario elige exactamente un workspace y marca "I approve connecting this assistant to …" (apruebo conectar este asistente a …). La aprobación nombra un workspace y nada más. Una conexión nueva no tiene proyectos. El propietario los añade después en la app (key icon → Connected → Add to project).&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué no basta con el scope?
&lt;/h2&gt;

&lt;p&gt;Un scope se fija cuando se emite el token. &lt;code&gt;osfoundry:write&lt;/code&gt; dice que la conexión puede hacer acciones de escritura. No dice qué Note, y no puede saber que esa Note se movió a otro proyecto hace diez minutos. Las notas de diseño de osFoundry enumeran lo que se vuelve a leer en cada llamada: estado de la conexión, workspace, pertenencia al proyecto, el proyecto actual y la ACL del recurso, pertenencia al canal o rol en la sesión, la herramienta y el scope OAuth. Además, la persona que aprobó la conexión tiene que seguir teniendo permisos de edición sobre el elemento. Quitar o reasignar algo "tiene efecto inmediato".&lt;/p&gt;

&lt;p&gt;La alternativa es meter la lista de proyectos en el token, o en una caché indexada por el token, y fiarse de ella hasta que caduque. Es más barato de escribir. La reconstrucción de abajo implementa las dos cosas para poder medir la diferencia de comportamiento.&lt;/p&gt;

&lt;h2&gt;
  
  
  La reconstrucción
&lt;/h2&gt;

&lt;p&gt;No hay ningún binario de Claude de por medio. &lt;code&gt;client.py&lt;/code&gt; hace de host y &lt;code&gt;server.py&lt;/code&gt; hace de servidor MCP. Los dos usan solo la biblioteca estándar, unas 280 líneas entre ambos, y se ejecutaron con Python 3.9.6 en un Apple M1. El servidor implementa &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt; y &lt;code&gt;tools/call&lt;/code&gt; sobre JSON-RPC en HTTP plano, los dos documentos well-known, &lt;code&gt;/authorize&lt;/code&gt; (la página de consentimiento se aprueba sola para un workspace) y &lt;code&gt;/token&lt;/code&gt; con PKCE S256. Unas cuantas rutas &lt;code&gt;/admin/*&lt;/code&gt; sustituyen a los clics del propietario en la pestaña Connected.&lt;/p&gt;

&lt;p&gt;La comprobación que se ejecuta antes de cada herramienta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TOOL_SCOPE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scopes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insufficient_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;CHECK&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                     &lt;span class="c1"&gt;# design B: trust issue-time state
&lt;/span&gt;        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects_at_issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONNECTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection_revoked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PROJECT_MEMBERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El cliente arranca sin token. Llama a una herramienta, recibe el 401, saca &lt;code&gt;resource_metadata&lt;/code&gt; de &lt;code&gt;WWW-Authenticate&lt;/code&gt;, descarga los dos documentos de metadatos, hace authorize y token con un verificador PKCE nuevo y luego recorre el escenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ CHECK=per_call python3 server.py &amp;amp;  python3 client.py 8767
discovery: first call -&amp;gt; 401, metadata /.well-known/oauth-protected-resource/mcp, 5 round trips, 11.3 ms
1 fresh connection, no project yet           200 OK   []
2 after owner adds project A                 200 OK   ["note-a1"]
3 read note in A                             200 OK   "Q3 launch checklist"
4 read note in B (never added)               200 DENY not_in_project
5 write with a read-only token               403 DENY insufficient_scope
6 write with read+write token                200 OK   "saved"
7 same read after note moved to B            200 DENY not_in_project
8 same read after owner removes project A    200 DENY not_in_project
9 same read after revoke                     200 DENY connection_revoked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El mismo cliente contra &lt;code&gt;CHECK=token_only&lt;/code&gt;. Aquí el token lleva la lista de proyectos de su último refresco:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7 same read after note moved to B            200 DENY not_in_project
8 same read after owner removes project A    200 OK   "Q3 launch checklist v2"
9 same read after revoke                     200 OK   "Q3 launch checklist v2"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los pasos 1–7 son idénticos. El paso 7 sigue denegado en el diseño solo-token porque el proyecto de la nota se consulta en vivo. El diseño solo-token solo se queda desfasado en los datos que viven dentro del token. Los pasos 8 y 9 son los que le importan al propietario: quitó el proyecto, luego revocó la conexión, y el chat siguió leyendo. En el diseño solo-token eso dura hasta que caduca el token, que en esta reconstrucción es &lt;code&gt;expires_in: 3600&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Las denegaciones se reparten en dos capas. Un scope que falta es un fallo a nivel de protocolo: 403 con &lt;code&gt;error="insufficient_scope"&lt;/code&gt; y el scope necesario en &lt;code&gt;WWW-Authenticate&lt;/code&gt;, para que el host pueda pedir más. Todo lo que tiene que ver con qué elemento está implicado es un resultado normal de herramienta con &lt;code&gt;isError: true&lt;/code&gt;. Volver a autorizar no lo arreglaría, así que no tiene sentido decirle al host que lo intente.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cuánto cuesta la comprobación en cada llamada?
&lt;/h2&gt;

&lt;p&gt;Medido con &lt;code&gt;time.perf_counter_ns()&lt;/code&gt; alrededor de &lt;code&gt;access()&lt;/code&gt; dentro del servidor, 1,000 llamadas por ejecución (500 permitidas, 500 denegadas), en un Mac que además estaba ejecutando otros trabajos:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;access() mediana&lt;/th&gt;
&lt;th&gt;access() p99&lt;/th&gt;
&lt;th&gt;llamada a herramienta en loopback, mediana&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;comprobación por llamada, 6 ejecuciones&lt;/td&gt;
&lt;td&gt;1.3–1.6 µs&lt;/td&gt;
&lt;td&gt;6.1–7.0 µs&lt;/td&gt;
&lt;td&gt;0.43–0.78 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;solo token, 3 ejecuciones&lt;/td&gt;
&lt;td&gt;0.7–1.6 µs&lt;/td&gt;
&lt;td&gt;3.5–7.7 µs&lt;/td&gt;
&lt;td&gt;0.35–1.36 ms (2 ejecuciones)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En memoria, la recomprobación se pierde en el ruido de una llamada HTTP por loopback. Un servidor real hace consultas a base de datos en lugar de comprobar pertenencia a un conjunto, así que su coste es mayor. Esta reconstrucción no puede medirlo, así que no se da ninguna cifra. Para hacerse una idea, el tramo de red hasta el endpoint en producción desde Tokio ronda los 500 ms a 800 ms por petición con urllib y una conexión TLS nueva cada vez (n=10 cada una: &lt;code&gt;initialize&lt;/code&gt; mediana 790 ms, &lt;code&gt;tools/list&lt;/code&gt; 816 ms, el &lt;code&gt;tools/call&lt;/code&gt; con 401 565 ms, cada documento de metadatos unos 520–530 ms). Un conector no pierde el tiempo en la comprobación de acceso.&lt;/p&gt;

&lt;p&gt;El descubrimiento en sí fueron 5 idas y vueltas HTTP en local: 401, metadatos del recurso, metadatos del servidor, authorize, token. Tardó 7.5–13.4 ms en siete ejecuciones y 34 ms una vez con el Mac ocupado. Contra el servidor en producción, los tres tramos GET/POST que puedes hacer sin credenciales suman aproximadamente 1.6 s en frío. El tramo de authorize espera a una persona de todos modos.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué Claude puede llamar a osFoundry, pero osFoundry no puede abrir tus chats de Claude?
&lt;/h2&gt;

&lt;p&gt;En este montaje quien llama es el host. Abre la conexión, lista las herramientas y decide cuándo llamar a una. El servidor responde. Nada en este montaje le da al servidor un acceso a las conversaciones del host. El plan de osFoundry lo dice en su tabla de capacidades: abrir o enumerar chats personales arbitrarios de ChatGPT/Claude es "No", con "No general host API is provided for this".&lt;/p&gt;

&lt;p&gt;Eso condiciona las herramientas. Con &lt;code&gt;link_current_chat&lt;/code&gt;, es el propio chat el que crea un enlace desde dentro. Su etiqueta la declara el propio chat, y osFoundry la usa para enrutar, no como identidad. El trabajo asignado a un chat de Claude enlazado espera hasta que el usuario abre ese chat y le pide que lo revise. ChatGPT tiene MCP Events para chats suscritos, y el plan de osFoundry lo trata como algo específico de ese host.&lt;/p&gt;

&lt;p&gt;El lado del servidor es dueño de todas las decisiones sobre datos, porque es el único que conoce el estado actual de un proyecto. El lado del host es dueño de todas las decisiones sobre cuándo actuar y si preguntar al usuario. La comprobación en cada llamada es lo que permite que el servidor siga siendo correcto cuando esos dos lados se desincronizan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Si lo montas tú
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pon &lt;code&gt;resource_metadata&lt;/code&gt; en el &lt;code&gt;WWW-Authenticate&lt;/code&gt; del 401 y sirve la RFC 9728 en &lt;code&gt;/.well-known/oauth-protected-resource/&amp;lt;resource path&amp;gt;&lt;/code&gt;. Un cliente que construye la URL a mano puede equivocarse. La cabecera se lo ahorra a todos los clientes.&lt;/li&gt;
&lt;li&gt;Decide qué es público antes del inicio de sesión. osFoundry expone &lt;code&gt;initialize&lt;/code&gt; y &lt;code&gt;tools/list&lt;/code&gt; y protege &lt;code&gt;tools/call&lt;/code&gt;. Si las descripciones de tus herramientas filtran algo sobre los tenants, protege también &lt;code&gt;tools/list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Declara un scope y &lt;code&gt;readOnlyHint&lt;/code&gt; por herramienta. Los hosts pueden construir con ellos las peticiones de confirmación, y un 403 &lt;code&gt;insufficient_scope&lt;/code&gt; le da al host algo concreto que volver a pedir.&lt;/li&gt;
&lt;li&gt;Mantén el token pequeño: quién, qué conexión, qué scopes. Vuelve a leer en cada llamada el workspace, la pertenencia al proyecto, el proyecto del recurso y el rol actual de quien aprobó, para que revocar y "mover a otro proyecto" funcionen en la siguiente llamada.&lt;/li&gt;
&lt;li&gt;Devuelve las denegaciones sobre recursos como resultados de herramienta, no como 401/403. Otro token no va a arreglar "esta Note está en el proyecto B".&lt;/li&gt;
&lt;li&gt;Haz que las escrituras lleven &lt;code&gt;expected_version&lt;/code&gt; y una clave de idempotencia. Los hosts reintentan, y los modelos repiten llamadas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La parte MCP de la especificación está en la &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;página de autorización de MCP&lt;/a&gt;. La definición breve de osFoundry está en su &lt;a href="https://osfoundry.io/es/glossary/mcp" rel="noopener noreferrer"&gt;entrada del glosario sobre MCP&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>claude</category>
      <category>seguridad</category>
    </item>
  </channel>
</rss>
