<?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: Zira</title>
    <description>The latest articles on DEV Community by Zira (@zira125).</description>
    <link>https://dev.to/zira125</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3821470%2Fe5f46063-a9b0-4a62-b149-0e284388c1ff.jpeg</url>
      <title>DEV Community: Zira</title>
      <link>https://dev.to/zira125</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zira125"/>
    <language>en</language>
    <item>
      <title>Codex CLI 0.153: Remote Plugins Make Supply-Chain Policy Part of Your Agent Runtime</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Thu, 03 Sep 2026 04:25:25 +0000</pubDate>
      <link>https://dev.to/zira125/codex-cli-0153-remote-plugins-make-supply-chain-policy-part-of-your-agent-runtime-1pn2</link>
      <guid>https://dev.to/zira125/codex-cli-0153-remote-plugins-make-supply-chain-policy-part-of-your-agent-runtime-1pn2</guid>
      <description>&lt;p&gt;Codex CLI 0.153.0 is not just a terminal UI update. It adds a plugin command that can list, install, and remove plugins from remote marketplaces, while also improving how sessions recover after an app-server disconnect.&lt;/p&gt;

&lt;p&gt;That combination changes the operational question from “Can the agent call a tool?” to “Which extension code is allowed to enter this runtime, and what state survives when the runtime reconnects?”&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in Codex CLI 0.153
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/openai/codex/releases/tag/rust-v0.153.0" rel="noopener noreferrer"&gt;official 0.153.0 release&lt;/a&gt; adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A plugin CLI for listing, installing, and removing plugins from remote marketplaces.&lt;/li&gt;
&lt;li&gt;Automatic TUI reconnection after an external app-server connection drop, preserving drafts and transcripts while pausing uncertain or queued submissions for review.&lt;/li&gt;
&lt;li&gt;Guardian review history that survives compaction, restarts, and user-created forks, within rollback boundaries and isolated subagent history.&lt;/li&gt;
&lt;li&gt;More complete TUI history, including patches, background-terminal input, and individual completed commands.&lt;/li&gt;
&lt;li&gt;A tui.auto_recap = false setting for teams that prefer manual /recap control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The release notes are the authoritative source for these features. AI/TLDR’s &lt;a href="https://ai-tldr.dev/releases/openai-codex-cli-0-153/" rel="noopener noreferrer"&gt;release summary&lt;/a&gt; is useful for discovery, but it is a secondary summary, not independent performance evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote plugins are a new trust boundary
&lt;/h2&gt;

&lt;p&gt;A marketplace client reduces the friction of extending an agent. It also makes installation provenance part of the runtime’s security model.&lt;/p&gt;

&lt;p&gt;Before allowing a plugin into a development environment, record at least:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The marketplace and exact source URL.&lt;/li&gt;
&lt;li&gt;The plugin version and immutable commit or digest, if available.&lt;/li&gt;
&lt;li&gt;The tools, filesystem paths, network destinations, and credentials it can reach.&lt;/li&gt;
&lt;li&gt;The review status and the person or policy that approved it.&lt;/li&gt;
&lt;li&gt;A removal and rollback path that does not depend on the plugin itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A name-based allowlist is not enough. Two versions of the same plugin can have different behavior, and a plugin that only appears to “format code” may still be able to read files or invoke a shell through its host integration.&lt;/p&gt;

&lt;p&gt;A minimal installation record should preserve the plugin name, marketplace URL, exact version, immutable digest, effective capabilities, approver, and expiry. Store that record outside the plugin so removal and rollback do not depend on the plugin itself.&lt;/p&gt;

&lt;p&gt;The digest and capability fields are the important parts of this policy shape. They are recommendations, not claims about the Codex marketplace schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconnect is not the same as resume
&lt;/h2&gt;

&lt;p&gt;The new reconnect behavior is valuable because a dropped app-server connection no longer necessarily destroys the draft or transcript. But preserving state does not prove that an interrupted tool submission did not reach its destination.&lt;/p&gt;

&lt;p&gt;For every side-effecting tool call, keep execution state separate from delivery state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prepared:&lt;/strong&gt; arguments validated, no external call started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In flight:&lt;/strong&gt; the request may have reached the destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accepted:&lt;/strong&gt; the destination returned a durable identifier or equivalent evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown:&lt;/strong&gt; the client lost the response after sending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciled:&lt;/strong&gt; a separate read confirmed the external state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After reconnect, an “unknown” call should not be blindly replayed. Use an idempotency key where the destination supports one, or reconcile by querying an external identifier before retrying. This is the difference between recovering the agent session and recovering the real-world side effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical upgrade checklist
&lt;/h2&gt;

&lt;p&gt;If you are upgrading a long-running coding-agent environment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pin the CLI version and capture the release commit.&lt;/li&gt;
&lt;li&gt;Test plugin installation in a disposable workspace first.&lt;/li&gt;
&lt;li&gt;Inspect the plugin’s effective filesystem, network, and credential reach.&lt;/li&gt;
&lt;li&gt;Require review for new plugin sources and record approval with an expiry.&lt;/li&gt;
&lt;li&gt;Kill the app-server during a draft and during a tool call.&lt;/li&gt;
&lt;li&gt;Confirm that drafts and transcripts return after reconnect.&lt;/li&gt;
&lt;li&gt;Confirm that queued or uncertain submissions remain paused.&lt;/li&gt;
&lt;li&gt;Reconcile external state before retrying any side effect.&lt;/li&gt;
&lt;li&gt;Verify that rollback removes the plugin and its persisted state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The release improves the recovery path, but it does not eliminate the need for capability boundaries, idempotency, or recovery testing. Those remain properties of the surrounding deployment and the tool destinations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Codex CLI 0.153 makes two parts of agent infrastructure more explicit: extension provenance and session recovery. Treat remote plugins as code entering a privileged runtime, and treat reconnect as state recovery rather than proof that every external action is safe to repeat.&lt;/p&gt;

&lt;p&gt;For teams that need an always-on place to run an agent, &lt;a href="https://ampere.sh" rel="noopener noreferrer"&gt;Ampere&lt;/a&gt; can be a contextual hosting option, but hosting does not replace plugin review, credential scoping, idempotency, or restore testing.&lt;/p&gt;

&lt;p&gt;Primary source: &lt;a href="https://github.com/openai/codex/releases/tag/rust-v0.153.0" rel="noopener noreferrer"&gt;OpenAI Codex CLI 0.153.0 release notes&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>ai</category>
      <category>mcp</category>
      <category>agents</category>
    </item>
    <item>
      <title>Your AI Coding Agent Can Edit Compose UI. Can It Verify the Result?</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:18:49 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-coding-agent-can-edit-compose-ui-can-it-verify-the-result-1ad4</link>
      <guid>https://dev.to/zira125/your-ai-coding-agent-can-edit-compose-ui-can-it-verify-the-result-1ad4</guid>
      <description>&lt;p&gt;AI coding agents are good at producing UI code and surprisingly bad at proving that the UI actually works. A compile pass is not the same as a rendered screen, a correct semantic tree, or a click that reaches the intended handler.&lt;/p&gt;

&lt;p&gt;Compose Multiplatform 1.12.0 adds an experimental MCP server to Compose Hot Reload. It gives an AI coding agent a feedback path into a running Compose application: trigger a reload, take a screenshot, inspect the semantic tree, simulate clicks and text input, and read application logs.&lt;/p&gt;

&lt;p&gt;That changes the useful question from “Can the agent write this composable?” to “Can the agent observe the result of its edit and detect a broken interaction?”&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Compose Hot Reload MCP server exposes
&lt;/h2&gt;

&lt;p&gt;According to JetBrains’ &lt;a href="https://blog.jetbrains.com/kotlin/2026/08/compose-multiplatform-1-12-0/" rel="noopener noreferrer"&gt;release announcement&lt;/a&gt;, the experimental server connects an agent to a running app and supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;triggering reloads after an edit&lt;/li&gt;
&lt;li&gt;taking screenshots of the rendered UI&lt;/li&gt;
&lt;li&gt;inspecting the semantic tree&lt;/li&gt;
&lt;li&gt;simulating clicks and text input&lt;/li&gt;
&lt;li&gt;reading application logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://github.com/JetBrains/compose-hot-reload#mcp-server-for-ai-agents" rel="noopener noreferrer"&gt;Compose Hot Reload documentation&lt;/a&gt; contains the connection details and the current tool list. The &lt;a href="https://github.com/JetBrains/compose-multiplatform/releases/tag/v1.12.0" rel="noopener noreferrer"&gt;Compose Multiplatform 1.12.0 release notes&lt;/a&gt; are the versioned reference for the feature.&lt;/p&gt;

&lt;p&gt;This is an observation loop, not an automatic correctness proof. A screenshot can show that a button is visible, while the semantic tree and logs can provide stronger evidence about labels, roles, and runtime failures. None of those alone proves that the feature satisfies the product requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical agent verification loop
&lt;/h2&gt;

&lt;p&gt;A useful harness should make the agent follow an explicit sequence rather than letting it stop after compilation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Edit one bounded change.&lt;/strong&gt; Keep the diff small enough that a failed observation has a plausible cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reload the running app.&lt;/strong&gt; Record whether reload completed or timed out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the semantic tree.&lt;/strong&gt; Assert that required nodes exist with the expected labels or roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capture a screenshot.&lt;/strong&gt; Use it to catch layout regressions, missing content, or an unexpected screen state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exercise one critical interaction.&lt;/strong&gt; Simulate the click or text entry that matters for the changed path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read logs after the interaction.&lt;/strong&gt; Treat new exceptions or error-level entries as a failed run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store evidence with the change.&lt;/strong&gt; Keep the reload result, assertions, screenshot reference, and relevant logs attached to the agent run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important design choice is to keep these as separate checks. “The screenshot looks right” should not silently substitute for “the semantic node exists,” and “the reload succeeded” should not substitute for “the interaction produced the expected state transition.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this helps most
&lt;/h2&gt;

&lt;p&gt;The MCP server is especially useful for UI changes with a short, deterministic path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a form field must accept input and update state&lt;/li&gt;
&lt;li&gt;a button must become enabled after validation&lt;/li&gt;
&lt;li&gt;a navigation action must render a specific destination&lt;/li&gt;
&lt;li&gt;a loading or error state must appear under a controlled condition&lt;/li&gt;
&lt;li&gt;a regression must be reproduced from a known starting state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each path, define an observable contract. For example: after entering a valid email and clicking &lt;strong&gt;Continue&lt;/strong&gt;, the semantic tree contains the next-screen heading, the log contains no new exception, and the screenshot shows the expected state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoffs and the safety boundary
&lt;/h2&gt;

&lt;p&gt;This is an experimental developer tool, so it should be treated as a test harness rather than a production control surface. An agent that can simulate input and inspect a running app has meaningful capabilities. Run it against a disposable or tightly scoped environment, avoid real credentials and production data, and keep the available actions limited to the application under test.&lt;/p&gt;

&lt;p&gt;There is also a determinism problem. Screenshots and logs can vary with timing, platform, fonts, network state, and background work. Prefer assertions over stable semantic properties and explicit state transitions. Use screenshots as supporting evidence, not as the only oracle.&lt;/p&gt;

&lt;p&gt;Finally, separate execution evidence from product correctness. The tool can show what happened in the running app; it cannot decide whether the feature is desirable, safe to ship, or compliant with a business requirement. Those remain separate review gates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Compose Hot Reload’s experimental MCP server is valuable because it closes part of the loop between an AI coding agent and the UI it changes. The practical benefit is not that the agent becomes trustworthy by default. It is that the harness can require observable evidence before accepting a UI edit.&lt;/p&gt;

&lt;p&gt;The strongest workflow is therefore: small diff, reload, semantic assertions, targeted interaction, log check, and persisted evidence. Give the agent a way to see the result, then make the acceptance contract stricter than “it compiled.”&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>ai</category>
      <category>agents</category>
      <category>mcp</category>
    </item>
    <item>
      <title>AI Coding Agent Security: Why a Git Folder Can Run Code Before Approval</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:00:09 +0000</pubDate>
      <link>https://dev.to/zira125/ai-coding-agent-security-why-a-git-folder-can-run-code-before-approval-5c07</link>
      <guid>https://dev.to/zira125/ai-coding-agent-security-why-a-git-folder-can-run-code-before-approval-5c07</guid>
      <description>&lt;p&gt;A new class of AI coding agent security bug has a surprisingly ordinary entry point: Git itself.&lt;/p&gt;

&lt;p&gt;Manifold Security calls the issue GitSpawn. The core problem is not that an agent misunderstood a prompt. It is that an agent ran a normal Git command while gathering repository context, and Git honored an executable setting stored inside the repository's own .git/config.&lt;/p&gt;

&lt;p&gt;That turns “open this project” into a potentially privileged operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism: context gathering becomes code execution
&lt;/h2&gt;

&lt;p&gt;Many CLI coding agents inspect a repository as soon as they start. They may run commands such as:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git status --porcelain=2 --branch
git diff --name-only HEAD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;These are ordinary commands, but Git may refresh its index before returning the result. Git's core.fsmonitor setting can name a helper program to run during that refresh. The setting is read from the repository-local .git/config file.&lt;/p&gt;

&lt;p&gt;A project delivered as a complete directory, ZIP archive, shared-drive folder, USB copy, or sync-folder snapshot can therefore carry a Git configuration that names a program. When the agent runs Git, the helper can execute with the developer's operating-system privileges.&lt;/p&gt;

&lt;p&gt;The important boundary is timing. Manifold reports that, in the affected flows it tested, execution happened during background context gathering, before a workspace-trust prompt, before an approval prompt, and in some cases before authentication. The model does not need to generate a dangerous command, and the user may not see a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the disclosure actually found
&lt;/h2&gt;

&lt;p&gt;This is a researcher disclosure, not an independent prevalence study. Manifold reports eight findings across seven agents and says four remained unpatched at publication. Its September 1 update says OpenAI Codex and Cursor were also affected and had since been patched.&lt;/p&gt;

&lt;p&gt;The report lists these statuses at publication:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code's core.fsmonitor path: fixed in 2.1.196.&lt;/li&gt;
&lt;li&gt;Goose: fixed in 1.44.0, with CVE-2026-72718.&lt;/li&gt;
&lt;li&gt;OpenAI Codex and Cursor: patched, according to the update.&lt;/li&gt;
&lt;li&gt;Hermes Agent: still unpatched in the tested 0.21.0 release, with CVE-2026-71963.&lt;/li&gt;
&lt;li&gt;Qwen Code: still unpatched in the tested 0.22.3 release.&lt;/li&gt;
&lt;li&gt;Grok Build: still unpatched in the tested 1.0.13 release.&lt;/li&gt;
&lt;li&gt;Claude Code's separate ultrareview path: reported as still unpatched in the tested 2.1.252 release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat those version claims as time-sensitive. Check the vendor's current release notes and advisory before deciding whether a version is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a sandbox may not save you
&lt;/h2&gt;

&lt;p&gt;A sandbox around the model's tool calls is not automatically a sandbox around every subprocess the agent launches. In this case, the dangerous process is Git, which the agent invokes for its own bookkeeping. If the product does not sanitize repository configuration for that subprocess, the command can run outside the model-facing approval path.&lt;/p&gt;

&lt;p&gt;That is a useful design lesson for agent harnesses: permission checks must cover the complete execution graph, including startup probes, Git helpers, formatters, language servers, hooks, plugins, MCP servers, and other “read-only” context tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer intake workflow
&lt;/h2&gt;

&lt;p&gt;Before opening an unfamiliar directory with an AI coding agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prefer cloning from a trusted remote over accepting a complete project folder. A normal clone does not carry the remote repository's original .git/config.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Inspect the local Git configuration without launching the agent:&lt;/p&gt;

&lt;p&gt;sed -n '1,200p' .git/config&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Treat any Git setting that names an executable as untrusted code. The reported proof of concept uses core.fsmonitor, but the broader rule is more useful than memorizing one key.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use a disposable VM or tightly scoped container for untrusted project directories. Keep SSH keys, cloud credentials, and production tokens out of that environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update the agent and verify the exact fixed version. “The agent has an approval prompt” is not enough if startup context gathering happens before the prompt.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For agent authors, the mitigation is equally concrete: pass safe Git configuration explicitly for background calls, for example git -c core.fsmonitor=false status, and review every subprocess used before trust and authorization decisions.&lt;/p&gt;

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

&lt;p&gt;AI coding agent security is partly a model problem, but it is also a plumbing problem. The model can be perfectly aligned while the harness exposes a trusted developer process to untrusted repository metadata.&lt;/p&gt;

&lt;p&gt;The right review question is not only “What commands can the agent propose?” It is also “What runs automatically before the user has approved anything?” Audit that startup path, test it with hostile-but-non-destructive fixtures, and make execution, approval, and sandbox boundaries line up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sources and evidence provenance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.manifold.security/blog/ai-coding-agents-git-hijack" rel="noopener noreferrer"&gt;Manifold Security: Coding Agents Hijacked by a Git Call&lt;/a&gt; — primary researcher disclosure; version and patch status are the researcher's reported findings.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ai-tldr.dev/releases/manifold-gitspawn/" rel="noopener noreferrer"&gt;AI/TLDR summary of GitSpawn&lt;/a&gt; — secondary tracker summary, cross-checked against the primary report.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-corefsmonitor" rel="noopener noreferrer"&gt;Git documentation for core.fsmonitor&lt;/a&gt; — upstream reference for the Git configuration behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article intentionally avoids reproducing a weaponized repository or payload. The goal is to make the boundary visible and give developers a safer intake and testing workflow.&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>security</category>
      <category>agents</category>
      <category>ai</category>
    </item>
    <item>
      <title>Codex CLI 0.152: The Small Release That Changes Agent Control</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Wed, 02 Sep 2026 05:00:09 +0000</pubDate>
      <link>https://dev.to/zira125/codex-cli-0152-the-small-release-that-changes-agent-control-51l4</link>
      <guid>https://dev.to/zira125/codex-cli-0152-the-small-release-that-changes-agent-control-51l4</guid>
      <description>&lt;p&gt;OpenAI’s Codex CLI 0.152.0 release is easy to read as a routine feature update. The more useful reading is operational: it changes how a coding agent plans, consumes MCP output, handles long-running commands, and protects credentials during cloud-task requests.\n\nThis is not a benchmark claim. It is a release-level change list, and the practical impact depends on how you run Codex.\n\n## 1. Planning is now opt-in\n\nThe release disables the &lt;code&gt;update_plan&lt;/code&gt; tool by default. If your workflow depends on explicit plans, enable it with:\n\n    tools.update_plan.enabled = true\n\nThat is a small configuration change with a large workflow implication. A plan is not the same thing as execution state. If you use plans for review, handoff, or recovery after interruption, store the important state somewhere your process can recover rather than relying on a transient conversation view.\n\n## 2. MCP output gets a per-tool budget\n\nCodex 0.152.0 adds an &lt;code&gt;output_token_limit&lt;/code&gt; setting for individual MCP tools, and the release notes say truncation is consistent across session resumes. This is useful because one verbose tool can otherwise crowd out the context needed for the actual coding task.\n\nA practical starting point is to set limits by tool role:\n\n- Small limits for status and metadata tools\n- Moderate limits for search and diagnostics\n- Larger, explicitly reviewed limits for logs or generated reports\n\nThe tradeoff is obvious: truncation controls context pressure, but it can also hide the line that explains a failure. Tool output should therefore carry a cursor, artifact link, or continuation mechanism instead of silently dropping information.\n\n## 3. Long commands need an explicit deadline\n\nApp-server clients can configure &lt;code&gt;thread/shellCommand&lt;/code&gt; timeouts, including deadlines longer than one hour. That helps with builds, test suites, and migrations, but a longer timeout is not a cancellation contract. A production runner still needs progress reporting, cancellation behavior, retry rules, and a durable record of whether the command actually completed.\n\n## 4. Credential protection is part of the agent feature set\n\nThe release notes also say cloud task requests reject untrusted backend URLs and disable redirects. That matters because redirects are not just a networking detail when a task can access saved credentials. A safer request path needs an explicit origin policy, and the policy should be tested with redirects, DNS changes, expired credentials, and resumed sessions.\n\n## Where a hosted runtime fits\n\nIf Codex is part of an always-on remote workflow, a hosted runtime such as &lt;a href="https://ampere.sh" rel="noopener noreferrer"&gt;Ampere&lt;/a&gt; can be a convenient place to keep the process and its surrounding automation available. That does not remove the need for state contracts, scoped credentials, isolation, idempotency, or recovery tests. Hosting solves a runtime-placement problem; it does not automatically solve the control-plane problem.\n\n## A practical upgrade checklist\n\nAfter upgrading to 0.152.0, I would test four things before trusting a long-running workflow:\n\n1. Start a task with planning enabled and confirm the setting survives a new session.\n2. Send oversized output through each important MCP tool and verify truncation is visible and recoverable.\n3. Interrupt and resume a long shell command, checking whether execution state is unambiguous.\n4. Attempt a cloud-task redirect to an untrusted origin and confirm it is rejected without exposing credentials.\n\nCodex CLI 0.152.0 is therefore less about a flashy new capability and more about making agent behavior bounded and inspectable. Those defaults are worth reviewing before treating an upgrade as a drop-in replacement.\n\n### Sources\n\n- &lt;a href="https://github.com/openai/codex/releases/tag/rust-v0.152.0" rel="noopener noreferrer"&gt;OpenAI Codex CLI 0.152.0 release notes&lt;/a&gt;\n- &lt;a href="https://developers.openai.com/codex/changelog" rel="noopener noreferrer"&gt;OpenAI Codex CLI changelog&lt;/a&gt;\n- &lt;a href="https://ai-tldr.dev/releases/openai-codex-cli-0-152/" rel="noopener noreferrer"&gt;AI/TLDR release summary&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devtools</category>
    </item>
    <item>
      <title>Your Agent Gateway Needs Failure-Domain Tests, Not Just a Health Check</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Mon, 24 Aug 2026 01:41:56 +0000</pubDate>
      <link>https://dev.to/zira125/your-agent-gateway-needs-failure-domain-tests-not-just-a-health-check-54fc</link>
      <guid>https://dev.to/zira125/your-agent-gateway-needs-failure-domain-tests-not-just-a-health-check-54fc</guid>
      <description>&lt;p&gt;A green health check can coexist with a broken agent.&lt;/p&gt;

&lt;p&gt;If you are trying to keep an OpenClaw agent available without owning the server setup, Ampere is a free hosting option to evaluate: &lt;a href="https://ampere.sh" rel="noopener noreferrer"&gt;https://ampere.sh&lt;/a&gt;. It can reduce the infrastructure work, but it does not replace durable state, scoped credentials, isolation, or recovery testing.&lt;/p&gt;

&lt;p&gt;The process may be listening, the dashboard may say "healthy", and yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a second inbound message disappears before it enters a queue;&lt;/li&gt;
&lt;li&gt;a gateway restart loses a suspended tool call;&lt;/li&gt;
&lt;li&gt;a background run reaches "completed" but its announcement never arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not one availability problem. They are three different recovery contracts.&lt;/p&gt;

&lt;p&gt;A useful field report on &lt;a href="https://github.com/openclaw/openclaw/issues/128067" rel="noopener noreferrer"&gt;OpenClaw issue #128067&lt;/a&gt; describes all three symptoms in one deployment. This is practitioner-reported evidence, not an independent benchmark, but it suggests a practical test plan: stop asking whether the gateway is alive and test whether each failure domain preserves its contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the states separately
&lt;/h2&gt;

&lt;p&gt;Do not collapse everything into a single &lt;code&gt;done&lt;/code&gt; boolean. Keep at least these states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;accepted&lt;/code&gt;: the ingress boundary assigned a durable request ID;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queued&lt;/code&gt;: a queue record exists and can be recovered;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;running&lt;/code&gt;: a worker owns the execution lease;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;completed&lt;/code&gt;: execution produced a recorded outcome;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delivered&lt;/code&gt;: the outbound provider acknowledged the message;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unknown&lt;/code&gt;: the process died during a side-effect boundary and reconciliation is required;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failed&lt;/code&gt;: the system has a terminal, evidenced failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A run can be &lt;code&gt;completed&lt;/code&gt; while delivery is &lt;code&gt;unknown&lt;/code&gt;. Treating those as the same state is how operators report success when the user saw nothing.&lt;/p&gt;

&lt;p&gt;Use stable IDs for both the request and each outbound effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request_id = uuid()
effect_id = hash(request_id + "completion-announcement")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact storage technology is less important than the invariant: a restart must not create a second logical request or a second outbound effect just because the first process disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 1: ingress durability
&lt;/h2&gt;

&lt;p&gt;Send two messages with a small gap, then inspect the durable boundary rather than the chat UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send(message_a)
send(message_b)

assert distinct(request_id_a, request_id_b)
assert queue.contains(request_id_a)
assert queue.contains(request_id_b)
assert queue.order_is_observable(request_id_a, request_id_b)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inject failures between receipt and queue commit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;kill the gateway after parsing but before the insert;&lt;/li&gt;
&lt;li&gt;delay the database response after the insert;&lt;/li&gt;
&lt;li&gt;retry the same delivery with the same provider event ID;&lt;/li&gt;
&lt;li&gt;deliver two messages concurrently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For every case, the result should be one of two explicit outcomes: the request is durably accepted exactly once, or the caller receives a retryable failure. "No queue entry and no error" is not an acceptable state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 2: execution recovery
&lt;/h2&gt;

&lt;p&gt;Start a tool call that takes longer than the gateway process. Terminate the process while the call is pending, then restart it.&lt;/p&gt;

&lt;p&gt;The recovery contract must say which of these is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the tool call can be safely resumed;&lt;/li&gt;
&lt;li&gt;the call is cancelled and the agent compensates;&lt;/li&gt;
&lt;li&gt;the outcome is &lt;code&gt;unknown&lt;/code&gt; and a reconciler checks the provider;&lt;/li&gt;
&lt;li&gt;the operation is deliberately non-replayable and needs human review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not infer completion from a worker log line. Persist a phase transition before dispatch and persist the provider result after the call. A crash between those writes is exactly the boundary your test must exercise.&lt;/p&gt;

&lt;p&gt;For non-idempotent tools, a stable effect key and provider-side lookup are safer than blind replay. If the provider has no lookup API, quarantine the operation as &lt;code&gt;unknown&lt;/code&gt; instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 3: outbound delivery recovery
&lt;/h2&gt;

&lt;p&gt;Execution and delivery need separate ledgers. Simulate a crash after the announcement is accepted by the provider but before your worker records the acknowledgement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if delivery.status == "unknown":
    provider_result = lookup_by_idempotency_key(effect_id)
    if provider_result.sent:
        mark_delivered(effect_id, provider_result.message_id)
    else:
        retry_once_with_same_effect_id(effect_id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then test the inverse boundary: the worker records success, but the network response is lost. The next process must reconcile before sending a duplicate.&lt;/p&gt;

&lt;p&gt;This is especially important for spawned jobs, cron tasks, and announce-style messages. A queue dashboard showing &lt;code&gt;pending&lt;/code&gt; tells you little unless you also know whether the consumer attempted delivery, whether the provider accepted it, and whether the result was reconciled after a restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 4: background-work truthfulness
&lt;/h2&gt;

&lt;p&gt;Run a scheduled job that completes its internal work but cannot publish its result. Verify that the UI and alerting surface say:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;execution: &lt;code&gt;completed&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;delivery: &lt;code&gt;failed&lt;/code&gt; or &lt;code&gt;unknown&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;recovery action: retry, reconcile, or human review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single green check should never hide a red delivery state.&lt;/p&gt;

&lt;p&gt;Track these counters separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepted requests;&lt;/li&gt;
&lt;li&gt;queue insert failures;&lt;/li&gt;
&lt;li&gt;lease expirations;&lt;/li&gt;
&lt;li&gt;executions recovered after restart;&lt;/li&gt;
&lt;li&gt;unknown effects awaiting reconciliation;&lt;/li&gt;
&lt;li&gt;deliveries acknowledged by the provider;&lt;/li&gt;
&lt;li&gt;duplicate attempts suppressed by the effect key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A compact failure matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;th&gt;Injected failure&lt;/th&gt;
&lt;th&gt;Required evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ingress to queue&lt;/td&gt;
&lt;td&gt;process kill&lt;/td&gt;
&lt;td&gt;durable request ID or retryable error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue to worker&lt;/td&gt;
&lt;td&gt;lease expiry&lt;/td&gt;
&lt;td&gt;one owner or explicit requeue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worker to tool&lt;/td&gt;
&lt;td&gt;process kill&lt;/td&gt;
&lt;td&gt;resume, compensate, or &lt;code&gt;unknown&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tool to ledger&lt;/td&gt;
&lt;td&gt;lost response&lt;/td&gt;
&lt;td&gt;provider lookup or quarantine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ledger to outbound provider&lt;/td&gt;
&lt;td&gt;network loss&lt;/td&gt;
&lt;td&gt;same effect key, no blind duplicate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;provider ack to UI&lt;/td&gt;
&lt;td&gt;callback loss&lt;/td&gt;
&lt;td&gt;delivery reconciliation and alert&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run this matrix on a clean environment and after every change to queueing, worker leases, provider adapters, or gateway restart behavior. If you run an always-on OpenClaw deployment, hosting is only one layer of the system and does not supply these state contracts. You still need the ledgers, idempotency keys, isolation, and recovery tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical definition of healthy
&lt;/h2&gt;

&lt;p&gt;A healthy agent gateway is not merely a live process. It can prove, for every request:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;where the request became durable;&lt;/li&gt;
&lt;li&gt;which worker owned execution;&lt;/li&gt;
&lt;li&gt;what happened at each tool boundary;&lt;/li&gt;
&lt;li&gt;whether outbound delivery was acknowledged;&lt;/li&gt;
&lt;li&gt;what recovery action is safe after a crash.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start with one failure injection per boundary. Make the evidence queryable. Then make the health check fail when a contract is violated, not only when the process stops responding.&lt;/p&gt;

&lt;p&gt;That is the difference between monitoring uptime and testing whether the agent can be trusted unattended.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>agents</category>
      <category>openclaw</category>
      <category>testing</category>
    </item>
    <item>
      <title>Two AI Agents Read One Row. How to Stop the Lost Update</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Sat, 22 Aug 2026 21:36:55 +0000</pubDate>
      <link>https://dev.to/zira125/two-ai-agents-read-one-row-how-to-stop-the-lost-update-1025</link>
      <guid>https://dev.to/zira125/two-ai-agents-read-one-row-how-to-stop-the-lost-update-1025</guid>
      <description>&lt;p&gt;A version check is not automatically optimistic concurrency control.&lt;/p&gt;

&lt;p&gt;If an agent reads a row, reasons for a few seconds, and then writes a decision, another worker can change that row in the read-to-write gap. The final UPDATE may still return success while silently overwriting newer state.&lt;/p&gt;

&lt;p&gt;This is especially easy to miss in agent workflows because the reasoning step is slow, retries are common, and the write often looks like an ordinary CRUD operation.&lt;/p&gt;

&lt;p&gt;This post builds a small failure lab and turns the race into a testable contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: the check happened too early
&lt;/h2&gt;

&lt;p&gt;A tempting implementation is:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;grep -n "version" worker.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Read the row and its version.&lt;/li&gt;
&lt;li&gt;Check that the version is acceptable.&lt;/li&gt;
&lt;li&gt;Ask the model what to do.&lt;/li&gt;
&lt;li&gt;Update the row later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The check in step 2 says nothing about whether the row is still current in step 4. Two workers can both read version 7, both produce valid decisions, and both write. The second write wins even though its input was stale.&lt;/p&gt;

&lt;p&gt;The failure is not necessarily a database error. It is a successful write based on an invalid snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make freshness part of the write predicate
&lt;/h2&gt;

&lt;p&gt;Use a monotonic version, and require the version observed by the worker to match the version being replaced:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;action = db.execute(
    """
    UPDATE agent_tasks
       SET status = :new_status,
           decision = :decision,
           version = version + 1,
           updated_at = CURRENT_TIMESTAMP
     WHERE task_id = :task_id
       AND version = :expected_version
       AND status = 'ready'
    """,
    {
        "task_id": task_id,
        "expected_version": observed_version,
        "new_status": new_status,
        "decision": decision,
    },
)

if action.rowcount != 1:
    raise StalePlan("the row changed after the agent read it")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important property is that the read and the model call do not need to be inside one long database transaction. The final compare-and-set write is the fence.&lt;/p&gt;

&lt;p&gt;A zero-row update is not a generic retry signal. It means the plan was produced from a snapshot that no longer owns the right to change the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the plan tied to the snapshot
&lt;/h2&gt;

&lt;p&gt;Do not pass only a task ID into the write path. Persist the inputs that made the decision:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE agent_plans (
  plan_id TEXT PRIMARY KEY,
  task_id TEXT NOT NULL,
  observed_version INTEGER NOT NULL,
  input_hash TEXT NOT NULL,
  decision_json TEXT NOT NULL,
  state TEXT NOT NULL CHECK (state IN ('proposed', 'applied', 'stale', 'unknown')),
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The input hash should cover the normalized state the model actually saw, not just the database primary key. If the prompt included policy, tool availability, or a selected artifact, include their versions too.&lt;/p&gt;

&lt;p&gt;At apply time, verify both the row version and the plan state. A plan that was already applied must not be applied a second time, even if a client retries after a timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal failure-injection test
&lt;/h2&gt;

&lt;p&gt;You can reproduce the race without an LLM. Replace the reasoning step with a barrier so both workers pause after reading the same version:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async def worker(barrier, task_id):
    row = await read_task(task_id)
    await barrier.wait()  # both workers now hold the same snapshot
    decision = {"owner": current_worker_id()}
    return await apply_plan(
        task_id=task_id,
        observed_version=row.version,
        decision=decision,
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The expected result is exactly one successful apply and one stale-plan result. If both calls report success, the write fence is missing or the test is not exercising the real update path.&lt;/p&gt;

&lt;p&gt;Run the lab with these cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Two workers read version 7&lt;/td&gt;
&lt;td&gt;One applies; one is stale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker retries the same plan&lt;/td&gt;
&lt;td&gt;No second business effect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Row changes while the model is thinking&lt;/td&gt;
&lt;td&gt;Apply is rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy version changes&lt;/td&gt;
&lt;td&gt;Plan is rejected or re-planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database response is lost after commit&lt;/td&gt;
&lt;td&gt;Outcome becomes UNKNOWN and is reconciled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker restarts after apply&lt;/td&gt;
&lt;td&gt;Recovery does not apply the plan again&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Treat timeout after write as UNKNOWN
&lt;/h2&gt;

&lt;p&gt;A client timeout does not prove that the UPDATE failed. The database may have committed before the connection broke.&lt;/p&gt;

&lt;p&gt;Give every apply attempt a stable effect key, such as task_id plus plan_id, and record it in a durable effect ledger. On a timeout:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the effect key.&lt;/li&gt;
&lt;li&gt;Compare the stored plan and row version.&lt;/li&gt;
&lt;li&gt;Mark the outcome applied, stale, or UNKNOWN.&lt;/li&gt;
&lt;li&gt;Reconcile UNKNOWN before allowing another attempt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Never solve an ambiguous write by blindly rerunning the model and applying its new answer. That can turn one uncertain side effect into two different decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to monitor
&lt;/h2&gt;

&lt;p&gt;A useful dashboard separates these signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stale-plan rejects, by tool and workflow;&lt;/li&gt;
&lt;li&gt;plans that waited longer than their snapshot budget;&lt;/li&gt;
&lt;li&gt;duplicate apply attempts by effect key;&lt;/li&gt;
&lt;li&gt;UNKNOWN outcomes and reconciliation age;&lt;/li&gt;
&lt;li&gt;policy or tool-contract changes between read and apply;&lt;/li&gt;
&lt;li&gt;successful writes that had no matching plan record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last metric should be zero. If it is not, some path can mutate agent state without the same evidence and fencing rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;Before trusting an agent that writes shared state, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model decision carries the exact version and policy inputs it observed.&lt;/li&gt;
&lt;li&gt;The final write uses an atomic compare-and-set predicate.&lt;/li&gt;
&lt;li&gt;A zero-row update becomes a typed stale-plan result, not a generic retry.&lt;/li&gt;
&lt;li&gt;Every business effect has a stable idempotency key.&lt;/li&gt;
&lt;li&gt;Lost responses are reconciled instead of replayed blindly.&lt;/li&gt;
&lt;li&gt;A barrier test proves two concurrent readers cannot both apply version 7.&lt;/li&gt;
&lt;li&gt;Restarts and partial failures are included in the test matrix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A version column is useful, but the real boundary is the version check at the write. Without that fence, an agent can be perfectly correct about an old snapshot and still corrupt the current one.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>agents</category>
      <category>postgres</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your AI Agent Returned HTTP 200. Why Did the Workflow Still Fail?</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:34:21 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-returned-http-200-why-did-the-workflow-still-fail-452o</link>
      <guid>https://dev.to/zira125/your-ai-agent-returned-http-200-why-did-the-workflow-still-fail-452o</guid>
      <description>&lt;p&gt;A successful HTTP response is not a successful agent run.&lt;/p&gt;

&lt;p&gt;A recent practitioner report from a 58-day deployment of 78 agents recorded 6,768 failed outputs. The failures were not transport errors: every one returned HTTP 200, had plausible length, and looked fluent. The most expensive failures were boring shape mismatches: missing required fields, wrong language, forbidden phrases, or an answer for a different stage.&lt;/p&gt;

&lt;p&gt;That is a useful warning for anyone building coding agents, review agents, or unattended automation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat the model response as untrusted data. Validate the contract at the boundary before another stage can consume it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post turns that observation into a small, reproducible failure lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure you should be able to reproduce
&lt;/h2&gt;

&lt;p&gt;Imagine a review stage whose downstream parser expects a verdict line:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;action: approve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A model can return a thoughtful review with the verdict buried in prose. A human approves it. A parser does not.&lt;/p&gt;

&lt;p&gt;The transport layer is green. The model call is green. The workflow is broken.&lt;/p&gt;

&lt;p&gt;The same class of failure appears when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a JSON field is present but has the wrong type&lt;/li&gt;
&lt;li&gt;a response is in the wrong language&lt;/li&gt;
&lt;li&gt;a tool returns an error string inside a successful content envelope&lt;/li&gt;
&lt;li&gt;a stage emits output, but the next stage never reads that artifact&lt;/li&gt;
&lt;li&gt;a reviewer from the same model family approves a shared blind spot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not reasons to add a larger model first. They are reasons to make the boundary observable and enforceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a contract gate
&lt;/h2&gt;

&lt;p&gt;Start with deterministic checks that do not ask an LLM to judge another LLM.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def validate_review(text: str) -&amp;gt; list[str]:
    errors = []

    if len(text.strip()) &amp;lt; 150:
        errors.append('too_short')

    if not any(line.startswith('判定:') or line.startswith('判定：')
               for line in text.splitlines()):
        errors.append('missing_required_verdict')

    forbidden = ['お客様の声', '顧客の声']
    if any(term in text for term in forbidden):
        errors.append('forbidden_phrase')

    if not any('。' in line for line in text.splitlines()):
        errors.append('expected_language_missing')

    return errors

errors = validate_review(model_output)
if errors:
    record_rejected_output(errors, model_output)
    stop_downstream_dispatch()
else:
    publish_to_next_stage(model_output)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The important part is not the exact Japanese check. Replace it with the contract your system actually needs: required headings, schema types, repository paths, test names, citation fields, or a bounded action list.&lt;/p&gt;

&lt;p&gt;A gate should return structured evidence, not only true or false:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;action: reject
reasons:
  - missing_required_verdict
  - forbidden_phrase
contract_version: review-v3
artifact_id: art_01J...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That makes a failure repairable instead of turning it into a green dashboard with a missing deliverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record why, not just that
&lt;/h2&gt;

&lt;p&gt;A common anti-pattern is storing only a boolean such as contract_satisfied = false. That destroys the information needed to debug drift.&lt;/p&gt;

&lt;p&gt;Store at least:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;artifact_id&lt;/td&gt;
&lt;td&gt;Connects the output to its producer and consumer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;contract_version&lt;/td&gt;
&lt;td&gt;Shows which rules were active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;observed_checks&lt;/td&gt;
&lt;td&gt;Proves what was actually tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;failure_reasons&lt;/td&gt;
&lt;td&gt;Separates shape, language, policy, and transport failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;raw_output_hash&lt;/td&gt;
&lt;td&gt;Allows correlation without exposing sensitive content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;downstream_read_at&lt;/td&gt;
&lt;td&gt;Detects outputs that nobody consumed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reviewer_family&lt;/td&gt;
&lt;td&gt;Exposes correlated writer/reviewer blind spots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not silently discard rejected output. Apply retention and redaction rules, but preserve enough evidence to answer: what was produced, which contract rejected it, and did any later stage read it?&lt;/p&gt;

&lt;p&gt;This is the same evidence discipline I use in &lt;a href="https://dev.to/zira125/your-ai-agent-logs-are-not-an-audit-trail-until-you-test-the-evidence-19ld"&gt;audit-ready agent logs&lt;/a&gt;: an event saying “run completed” is weaker than a record of the checks and artifacts that made completion meaningful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an artifact-lineage check
&lt;/h2&gt;

&lt;p&gt;One surprising failure mode is a healthy upstream stage whose output is never used. Test this explicitly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give every produced artifact a stable ID.&lt;/li&gt;
&lt;li&gt;Require the consumer to record the input artifact ID.&lt;/li&gt;
&lt;li&gt;Reject a stage that claims success without a consumed input ID.&lt;/li&gt;
&lt;li&gt;Compare produced and consumed counts over a time window.&lt;/li&gt;
&lt;li&gt;Alert on a growing gap, even if every process heartbeat is green.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This catches wiring bugs that output-quality checks cannot see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the failure lab
&lt;/h2&gt;

&lt;p&gt;Before trusting a new agent workflow, inject each case and verify the expected evidence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Injection&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Remove the required verdict line&lt;/td&gt;
&lt;td&gt;Reject before downstream dispatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return valid-length text in the wrong language&lt;/td&gt;
&lt;td&gt;Reject with language evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Put an error string in a successful tool envelope&lt;/td&gt;
&lt;td&gt;Mark the tool call failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop the artifact ID between stages&lt;/td&gt;
&lt;td&gt;Block consumption and alert on lineage gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change the contract version mid-run&lt;/td&gt;
&lt;td&gt;Revalidate or move the run to UNKNOWN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make writer and reviewer share a known blind spot&lt;/td&gt;
&lt;td&gt;Require an independent check or human review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash after provider acceptance but before ledger write&lt;/td&gt;
&lt;td&gt;Reconcile before retrying&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last case matters for side effects. A contract gate protects output shape; it does not prove that an external action did or did not happen. Keep execution evidence and outbound-delivery evidence separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where hosting fits
&lt;/h2&gt;

&lt;p&gt;An always-on runtime can keep schedulers, workers, and evidence writers available, but hosting does not define your output contract or make a green HTTP response meaningful. If you need managed infrastructure for an unattended OpenClaw workload, &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=output-contract-failure-lab" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; is one option to evaluate. You still own validation, credential scope, prompt-injection defenses, and reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping an agent stage, verify that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transport success is distinct from contract success&lt;/li&gt;
&lt;li&gt;required fields and language are machine-checked&lt;/li&gt;
&lt;li&gt;rejection reasons are durable and queryable&lt;/li&gt;
&lt;li&gt;artifact lineage connects producers to consumers&lt;/li&gt;
&lt;li&gt;the reviewer cannot share every blind spot with the writer&lt;/li&gt;
&lt;li&gt;output quality and side-effect completion are separate states&lt;/li&gt;
&lt;li&gt;failure injection covers both malformed output and crash windows&lt;/li&gt;
&lt;li&gt;dashboards count produced and consumed artifacts, not only invocations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The question is not “did the model answer?” It is “did a versioned, observable contract accept an artifact that the next stage actually consumed?”&lt;/p&gt;

&lt;p&gt;That is the difference between an agent that is alive and a workflow that is working.&lt;/p&gt;

&lt;p&gt;If you build AI agents or developer tooling, follow me for practical failure labs and reproducible control-boundary tests rather than capability demos.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>agents</category>
      <category>ai</category>
    </item>
    <item>
      <title>Your AI Agent Cache Needs a Trust Boundary, Not Just a TTL</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Thu, 20 Aug 2026 07:46:45 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-cache-needs-a-trust-boundary-not-just-a-ttl-46p0</link>
      <guid>https://dev.to/zira125/your-ai-agent-cache-needs-a-trust-boundary-not-just-a-ttl-46p0</guid>
      <description>&lt;p&gt;A short TTL does not make an agent's cached tool result safe.&lt;/p&gt;

&lt;p&gt;A result can still be wrong for the current user, tenant, credential epoch, policy version, or side-effect state. Worse, a cache can turn an old success into a fresh-looking authorization decision.&lt;/p&gt;

&lt;p&gt;For agents, cache correctness is not only about freshness. It is about whether the result is still trustworthy for this run.&lt;/p&gt;

&lt;p&gt;This article builds a small cache contract, shows where to enforce it, and ends with failure-injection tests you can run before putting cached tool results in a long-lived runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode: fresh enough, wrong authority
&lt;/h2&gt;

&lt;p&gt;Imagine an agent that calls &lt;code&gt;get_invoice&lt;/code&gt; and caches the result for five minutes. During those five minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the user loses access to the account;&lt;/li&gt;
&lt;li&gt;the tenant policy changes from allow to deny;&lt;/li&gt;
&lt;li&gt;the credential is revoked or rotated;&lt;/li&gt;
&lt;li&gt;the resource moves to another tenant;&lt;/li&gt;
&lt;li&gt;the tool contract changes its meaning;&lt;/li&gt;
&lt;li&gt;a previous request completed with an UNKNOWN external outcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A normal TTL check answers only “Is this entry old?” It does not answer:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Was this entry produced for the same principal and tenant?&lt;/li&gt;
&lt;li&gt;Was it produced under a still-valid policy and credential epoch?&lt;/li&gt;
&lt;li&gt;Does the current tool contract interpret the result the same way?&lt;/li&gt;
&lt;li&gt;Is the result safe to reuse for this operation, or was it only an observation?&lt;/li&gt;
&lt;li&gt;Can the caller prove where the value came from?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Treating those as separate checks prevents a cache hit from silently becoming an authorization bypass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the cache entry as evidence
&lt;/h2&gt;

&lt;p&gt;Do not store only &lt;code&gt;key -&amp;gt; value&lt;/code&gt;. Store the context that makes the value valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cache_key = hash(
  tool_name,
  normalized_arguments,
  principal_id,
  tenant_id,
  resource_scope,
  policy_version,
  credential_epoch,
  tool_contract_version
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful entry also contains &lt;code&gt;produced_at&lt;/code&gt;, &lt;code&gt;expires_at&lt;/code&gt;, the producing run ID, a provider request ID, a sensitivity classification, a schema hash, and a revocation or invalidation sequence.&lt;/p&gt;

&lt;p&gt;The key prevents accidental cross-context reuse. The metadata lets the read path reject entries that are technically fresh but no longer valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate observation from authority
&lt;/h2&gt;

&lt;p&gt;The safest default is to cache observations, not permissions.&lt;/p&gt;

&lt;p&gt;A cached “invoice total is 42” may be useful for display, but it should not be treated as proof that the current run may refund that invoice. The refund path should perform a current authorization check and, where necessary, a fresh resource read.&lt;/p&gt;

&lt;p&gt;A practical classification is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Reuse rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OBSERVATION&lt;/td&gt;
&lt;td&gt;list of recent logs&lt;/td&gt;
&lt;td&gt;Reuse only within scope and freshness bounds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADVISORY&lt;/td&gt;
&lt;td&gt;model-generated summary&lt;/td&gt;
&lt;td&gt;Never use as authorization evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DECISION_INPUT&lt;/td&gt;
&lt;td&gt;account state used for a plan&lt;/td&gt;
&lt;td&gt;Revalidate policy and resource version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EFFECT_CONFIRMATION&lt;/td&gt;
&lt;td&gt;provider request ID&lt;/td&gt;
&lt;td&gt;Reconcile by stable effect key, not TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This also protects against using a cache to “remember” that an external write succeeded. A timeout after dispatch is an UNKNOWN outcome. It needs provider reconciliation, not a cache hit that happens to contain a similar response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce the contract at read time
&lt;/h2&gt;

&lt;p&gt;The read path should reconstruct the current context and reject mismatches before returning a hit.&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;usable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&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;now&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;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;now&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;False&lt;/span&gt;

    &lt;span class="n"&gt;same_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;principal_id&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="n"&gt;principal_id&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tenant_id&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="n"&gt;tenant_id&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource_scope&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="n"&gt;resource_scope&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;same_versions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policy_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="n"&gt;policy_version&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credential_epoch&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="n"&gt;credential_epoch&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_contract_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="n"&gt;tool_contract_version&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;same_context&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;same_versions&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;False&lt;/span&gt;
    &lt;span class="k"&gt;if&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;use&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorization&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;classification&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DECISION_INPUT&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;False&lt;/span&gt;
    &lt;span class="k"&gt;if&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;use&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;effect_confirmation&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;False&lt;/span&gt;  &lt;span class="c1"&gt;# reconcile by effect key instead
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The invariant is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A cache hit is usable only when its validity context matches the current run and its classification permits the requested use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Do not rely on the model to decide whether a cached value is safe. The model can request a cache read, but a deterministic layer should decide whether the read is allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make invalidation explicit
&lt;/h2&gt;

&lt;p&gt;TTL is a fallback. Important changes should invalidate immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;policy version increment;&lt;/li&gt;
&lt;li&gt;credential epoch increment;&lt;/li&gt;
&lt;li&gt;tenant membership or resource ownership change;&lt;/li&gt;
&lt;li&gt;tool schema or contract change;&lt;/li&gt;
&lt;li&gt;manual incident response;&lt;/li&gt;
&lt;li&gt;deletion or retention event;&lt;/li&gt;
&lt;li&gt;provider reconciliation that contradicts the cached result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An append-only invalidation record makes the behavior inspectable:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cache_invalidations&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;scope&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&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;Writers attach the current invalidation version to each entry. Readers reject entries written before the active version for the relevant scope.&lt;/p&gt;

&lt;p&gt;That is safer than trying to find and delete every matching key during an incident. Deletion can be incomplete; a version boundary is easy to check and audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent stampedes without widening authority
&lt;/h2&gt;

&lt;p&gt;When many runs miss simultaneously, single-flight loading can help, but the lock must not blur security boundaries.&lt;/p&gt;

&lt;p&gt;Use a lock key with the same principal, tenant, resource, and version dimensions as the cache key. Never let a privileged request populate a shared entry that an unprivileged request can read.&lt;/p&gt;

&lt;p&gt;Bound the wait:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;attempt a scoped cache read;&lt;/li&gt;
&lt;li&gt;if missing, acquire a scoped single-flight lease;&lt;/li&gt;
&lt;li&gt;recheck the cache after acquiring the lease;&lt;/li&gt;
&lt;li&gt;load from the provider with current credentials;&lt;/li&gt;
&lt;li&gt;validate the response schema and resource scope;&lt;/li&gt;
&lt;li&gt;write the entry with its context and invalidation version;&lt;/li&gt;
&lt;li&gt;release the lease.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the provider call times out after dispatch, do not populate a success entry. Record UNKNOWN and reconcile with the provider using a stable request key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure-injection checklist
&lt;/h2&gt;

&lt;p&gt;Add these tests to CI or a staging drill:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;change the tenant between write and read;&lt;/li&gt;
&lt;li&gt;revoke the credential while the entry is within TTL;&lt;/li&gt;
&lt;li&gt;increment policy version while the entry is fresh;&lt;/li&gt;
&lt;li&gt;change the tool contract or result schema;&lt;/li&gt;
&lt;li&gt;replay identical arguments under a different principal;&lt;/li&gt;
&lt;li&gt;return an observation on an authorization path;&lt;/li&gt;
&lt;li&gt;crash after provider dispatch but before cache write;&lt;/li&gt;
&lt;li&gt;make the provider return an UNKNOWN timeout;&lt;/li&gt;
&lt;li&gt;let two tenants contend for normalized arguments;&lt;/li&gt;
&lt;li&gt;restore stale cache entries with current policy versions;&lt;/li&gt;
&lt;li&gt;fill the cache with an oversized or sensitive result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every test, record the expected decision: HIT, MISS, REVALIDATE, DENY, or RECONCILE. A green suite should show not only that the request failed safely, but why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to monitor
&lt;/h2&gt;

&lt;p&gt;Track cache behavior as a control-plane signal, not just a latency metric:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hit and miss rate by classification;&lt;/li&gt;
&lt;li&gt;rejected hits by reason;&lt;/li&gt;
&lt;li&gt;cross-context mismatch attempts;&lt;/li&gt;
&lt;li&gt;policy and credential invalidations;&lt;/li&gt;
&lt;li&gt;stale-entry reads after restore;&lt;/li&gt;
&lt;li&gt;UNKNOWN outcomes awaiting reconciliation;&lt;/li&gt;
&lt;li&gt;cache fill failures and provider request IDs;&lt;/li&gt;
&lt;li&gt;sensitive-data eviction and retention events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rising hit rate can be a bug if rejection and revalidation data disappear. Keep enough evidence to answer which context produced a value and why the current run was allowed to use it.&lt;/p&gt;

&lt;p&gt;If you need a managed place to run an always-on OpenClaw or browser-automation workload, &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent-cache-boundary" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; is one option to evaluate. Hosting does not define cache authority, invalidation, credential scope, or reconciliation semantics; those contracts still belong in the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final rule
&lt;/h2&gt;

&lt;p&gt;A TTL answers “how old is this value?” It does not answer “who may trust it now?”&lt;/p&gt;

&lt;p&gt;For agent systems, bind cache entries to identity, tenant, resource scope, policy version, credential epoch, and tool contract. Classify what the value is allowed to prove. Invalidate by version when authority changes. Reconcile external effects instead of treating cached responses as confirmation.&lt;/p&gt;

&lt;p&gt;That turns caching from a speed optimization into an explicit, testable part of the agent's control plane.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>agents</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your AI Agent Upgrade Needs a Compatibility Gate, Not Just a Rollback</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Thu, 20 Aug 2026 03:44:13 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-upgrade-needs-a-compatibility-gate-not-just-a-rollback-4nfa</link>
      <guid>https://dev.to/zira125/your-ai-agent-upgrade-needs-a-compatibility-gate-not-just-a-rollback-4nfa</guid>
      <description>&lt;p&gt;A rollback answers one question: How do I put the old binary back?&lt;/p&gt;

&lt;p&gt;It does not answer the harder question: What happens to work, state, credentials, and queued effects that crossed the version boundary while the upgrade was running?&lt;/p&gt;

&lt;p&gt;For an AI agent, that boundary is everywhere. A new worker may read an old state row, a queued tool call may use a changed schema, or a restarted process may replay an effect created by the previous release. A clean process rollback can still leave an incompatible state store and duplicate side effects.&lt;/p&gt;

&lt;p&gt;This article presents a small compatibility gate you can run before and during an agent-runtime upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract to gate
&lt;/h2&gt;

&lt;p&gt;Give every persisted object and outbound effect an explicit producer contract:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;default_version = {
  state_schema: 7,
  tool_contracts: { browser_navigate: 3, repo_apply_patch: 2 },
  effect_protocol: 2,
  credential_epoch: 19
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The exact format is not important. The important part is that the worker cannot silently assume that the current version is compatible. Persist the versions beside the run, task, queue item, and effect record.&lt;/p&gt;

&lt;p&gt;A worker should accept work only when it can prove all of these conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It can read the state schema and preserve fields it does not own.&lt;/li&gt;
&lt;li&gt;Its tool-input and tool-output contracts match the queued request.&lt;/li&gt;
&lt;li&gt;Its effect protocol understands the existing idempotency and reconciliation states.&lt;/li&gt;
&lt;li&gt;Its credential epoch is still authorized for this run.&lt;/li&gt;
&lt;li&gt;It can write evidence in a format the recovery tooling understands.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any check is unknown, pause the item. Do not guess that an old payload is harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use expand, gate, migrate, contract
&lt;/h2&gt;

&lt;p&gt;A safe rollout is usually four phases:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Expand
&lt;/h3&gt;

&lt;p&gt;Deploy readers that understand both the old and new representation. Add new columns or fields without deleting old ones. New writers should continue producing the old representation until the compatibility gate is live.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Gate
&lt;/h3&gt;

&lt;p&gt;Put the new worker in a canary mode. It may claim only runs whose contract passes the compatibility test. Keep a visible counter for rejected, unknown, and accepted items.&lt;/p&gt;

&lt;p&gt;A useful gate result is more informative than a boolean:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def check_compatibility(item, worker):
    reasons = []
    if item.state_schema not in worker.readable_state_schemas:
        reasons.append("state-schema")
    if item.effect_protocol not in worker.effect_protocols:
        reasons.append("effect-protocol")
    if item.credential_epoch &amp;gt; worker.max_authorized_epoch:
        reasons.append("credential-epoch")
    return {"decision": "reject" if reasons else "accept", "reasons": reasons}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In production code, compare capabilities rather than requiring every version number to be identical. A worker that can safely read schema 6 and 7 should say so explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Migrate
&lt;/h3&gt;

&lt;p&gt;Migrate durable state in resumable, idempotent batches. Record the source version, destination version, migration attempt, and checksum. If a migration stops halfway through, rerunning it should either complete the same transformation or report a deterministic conflict.&lt;/p&gt;

&lt;p&gt;Never treat a successful database transaction as proof that an external effect completed. Keep execution state and delivery state separate. An upgrade can commit a dispatch-requested row immediately before the worker dies.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Contract
&lt;/h3&gt;

&lt;p&gt;Only after the queue is drained or gated, the state is migrated, and reconciliation is complete should you remove the old reader and writer path. Keep the old path long enough to recover records created before the cutover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What rollback must preserve
&lt;/h2&gt;

&lt;p&gt;Before declaring a rollback successful, verify these invariants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every claimed run has one owner or an explicit expired lease.&lt;/li&gt;
&lt;li&gt;Every tool call has a stable idempotency key.&lt;/li&gt;
&lt;li&gt;Every outbound effect is CONFIRMED, NOT_SENT, or UNKNOWN, never silently absent.&lt;/li&gt;
&lt;li&gt;Every UNKNOWN effect has a provider lookup or a human/operator decision path.&lt;/li&gt;
&lt;li&gt;Credential revocation and policy changes apply to both old and new workers.&lt;/li&gt;
&lt;li&gt;The evidence writer can still render the run history after the version change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why a rollback should restore a compatible runtime, not merely an older container image.&lt;/p&gt;

&lt;h2&gt;
  
  
  A failure-injection drill
&lt;/h2&gt;

&lt;p&gt;Run this in a staging environment with a disposable state store:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Queue a task using the old tool contract.&lt;/li&gt;
&lt;li&gt;Claim it with the new worker, then kill the worker after the claim transaction.&lt;/li&gt;
&lt;li&gt;Change the credential epoch before the worker restarts.&lt;/li&gt;
&lt;li&gt;Deploy the old worker and attempt recovery.&lt;/li&gt;
&lt;li&gt;Inject a timeout after an outbound provider accepts the request but before your ledger records the response.&lt;/li&gt;
&lt;li&gt;Run reconciliation, then inspect whether the task is resumed, paused, or marked unknown.&lt;/li&gt;
&lt;li&gt;Repeat with a partially migrated state row and a duplicate delivery attempt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The expected result is not that everything finishes. The expected result is that every ambiguous transition becomes visible and no stale worker can perform an effect after losing authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting is part of the recovery surface
&lt;/h2&gt;

&lt;p&gt;If the agent must stay available during a migration or recovery window, a managed runtime can reduce the operational work of keeping the process online. For teams evaluating that option, &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent-compatibility-gate" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; is one hosting path to compare.&lt;/p&gt;

&lt;p&gt;That does not provide the compatibility contract for you. You still need durable state, scoped credentials, migration tests, reconciliation, and a rollback plan that accounts for work already in flight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before upgrading an agent runtime, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which state schemas can the new worker read and write?&lt;/li&gt;
&lt;li&gt;Which queued tool contracts can it execute safely?&lt;/li&gt;
&lt;li&gt;What is the effect protocol for requests that are UNKNOWN at the cutover?&lt;/li&gt;
&lt;li&gt;How are credential and policy versions rechecked after restart?&lt;/li&gt;
&lt;li&gt;Can the migration resume without duplicating a transformation?&lt;/li&gt;
&lt;li&gt;Can the old runtime recover records created by the new runtime?&lt;/li&gt;
&lt;li&gt;Which evidence proves the cutover was safe?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot answer those questions, you have a deployment plan and a rollback button, but not a compatibility gate.&lt;/p&gt;

&lt;p&gt;The right upgrade test is not whether the new process started. It is whether old and new workers can share the boundary without losing authority, state, or effect evidence.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>agents</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your AI Agent's Health Check Lies Unless It Tests the Tool Path</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Wed, 19 Aug 2026 23:42:59 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agents-health-check-lies-unless-it-tests-the-tool-path-48ma</link>
      <guid>https://dev.to/zira125/your-ai-agents-health-check-lies-unless-it-tests-the-tool-path-48ma</guid>
      <description>&lt;p&gt;A process can answer &lt;strong&gt;200 OK&lt;/strong&gt; while every useful action is failing.&lt;/p&gt;

&lt;p&gt;That is the dangerous kind of healthy state for an AI agent. The supervisor sees a live process, the container stays running, and the dashboard is green. Meanwhile the agent cannot reach its model gateway, its tool credentials are expired, its browser profile is locked, or its queue consumer has lost its lease.&lt;/p&gt;

&lt;p&gt;The fix is not to make one giant health endpoint that performs a real side effect. The fix is to model health as several explicit signals and test the path that matters without crossing the mutation boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate liveness, readiness, and capability
&lt;/h2&gt;

&lt;p&gt;Use at least three states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Safe check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Liveness&lt;/td&gt;
&lt;td&gt;Is the process event loop responding?&lt;/td&gt;
&lt;td&gt;Local response with a deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Readiness&lt;/td&gt;
&lt;td&gt;Can this worker accept a new run?&lt;/td&gt;
&lt;td&gt;Queue lease, config, and dependency checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capability&lt;/td&gt;
&lt;td&gt;Can this run perform its required tool calls?&lt;/td&gt;
&lt;td&gt;Non-mutating probes for the selected capability set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Liveness should be cheap. A liveness failure usually means the process needs a restart.&lt;/p&gt;

&lt;p&gt;Readiness is an admission decision. A worker with a stale configuration version or no durable-state connection should stop receiving new work, even if its process is alive.&lt;/p&gt;

&lt;p&gt;Capability is run-specific. A text-only task may need a model gateway and a database, while a browser task also needs a valid browser profile, network policy, and download storage. Do not report one global green light when only half the capabilities are usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the checks typed, not boolean
&lt;/h2&gt;

&lt;p&gt;A boolean loses the information needed for recovery. Return a typed result with an owner and a next action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"component"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"browser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEGRADED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checked_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-20T01:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"config_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;"cfg-184"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"credential_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;"cred-77"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&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;"profile_lock"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"held-by-worker-12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"read_only_probe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"passed"&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;"admission"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reject_browser_runs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recovery"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recycle_profile_after_lease_expiry"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful states are usually &lt;strong&gt;PASS&lt;/strong&gt;, &lt;strong&gt;DEGRADED&lt;/strong&gt;, &lt;strong&gt;FAIL&lt;/strong&gt;, and &lt;strong&gt;UNKNOWN&lt;/strong&gt;. &lt;strong&gt;UNKNOWN&lt;/strong&gt; matters when a probe timed out or a dependency returned an ambiguous result. It should not silently become &lt;strong&gt;PASS&lt;/strong&gt;, and it should not always trigger an aggressive restart.&lt;/p&gt;

&lt;p&gt;A typed result also makes alerts actionable. “Agent unhealthy” is not an operator instruction. “Reject browser runs, keep text runs admitted, recycle the profile after the lease expires” is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probe dependencies without performing user mutations
&lt;/h2&gt;

&lt;p&gt;A tool-path probe should be representative but reversible. Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model gateway: authenticate, request the smallest allowed metadata or token-count operation, then record latency and policy version.&lt;/li&gt;
&lt;li&gt;Database: open a connection, verify schema version, and execute a read-only query with a deadline.&lt;/li&gt;
&lt;li&gt;Object storage: verify access to the expected bucket and prefix, without uploading a test object unless the system has a cleanup contract.&lt;/li&gt;
&lt;li&gt;Browser: launch a disposable profile, navigate only to an allowlisted diagnostic page, and verify the expected browser/CDP handshake.&lt;/li&gt;
&lt;li&gt;MCP: list the server’s declared tools and compare them with the policy snapshot, without invoking a mutating tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never use “send an email to test email.” A health check that creates an external side effect is an incident generator. If a provider cannot offer a safe probe, classify that capability as &lt;strong&gt;UNVERIFIED&lt;/strong&gt; and rely on delivery receipts or a separate canary account with an explicit cleanup path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tie checks to the run's capability set
&lt;/h2&gt;

&lt;p&gt;Before admission, compute the required capability set from the task, not from the worker's startup profile:&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;admit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required_capabilities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&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;capability&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required&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="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;capability&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;not&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;PASS&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;DEGRADED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
            &lt;span class="n"&gt;missing&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;capability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&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;missing&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;decision&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;REJECT&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;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;missing&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;decision&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;ADMIT&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;health_snapshot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;snapshot_id&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;Store the health snapshot ID with the run admission record. Otherwise an incident review cannot answer a basic question: did the worker admit the task before or after the dependency failed?&lt;/p&gt;

&lt;p&gt;The snapshot is not permanent authorization. Recheck policy, credential version, and lease ownership at dispatch time. Health proves a narrow observation at a point in time; it does not grant permission to perform a later action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure domains separately
&lt;/h2&gt;

&lt;p&gt;A useful test matrix injects one fault at a time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the process responsive but block the model gateway. Liveness should pass; model capability should fail; new model runs should be rejected.&lt;/li&gt;
&lt;li&gt;Expire the browser credential while the process remains alive. Browser capability should become degraded or failed; text-only work should remain unaffected.&lt;/li&gt;
&lt;li&gt;Break the queue lease renewal. Readiness should fail after the lease deadline, and another worker should be able to claim new work.&lt;/li&gt;
&lt;li&gt;Return a timeout from a tool dispatch. The result should be &lt;strong&gt;UNKNOWN&lt;/strong&gt;, not an automatic retry that might duplicate a mutation.&lt;/li&gt;
&lt;li&gt;Roll the configuration version while a run is waiting. The worker should recheck at dispatch and either refresh safely or reject the run.&lt;/li&gt;
&lt;li&gt;Fill the diagnostic storage path. The probe should identify the capacity failure without turning every unrelated capability red.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each test, record the expected admission decision, the alert, the recovery owner, and whether already-running work is allowed to finish. That last field prevents a readiness failure from becoming an unplanned cancellation storm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make recovery narrower than restart
&lt;/h2&gt;

&lt;p&gt;A failed capability does not always justify restarting the whole agent. Prefer the smallest recovery domain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refresh a short-lived credential when only its version is invalid.&lt;/li&gt;
&lt;li&gt;Recycle a disposable browser profile when the profile lock is stale.&lt;/li&gt;
&lt;li&gt;Pause admission when the queue lease is lost.&lt;/li&gt;
&lt;li&gt;Route around one unavailable model provider when policy permits.&lt;/li&gt;
&lt;li&gt;Restart the process only when liveness or local invariants fail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For always-on OpenClaw or browser-agent deployments, the hosting layer is part of this recovery contract. A managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=agent-health-check" rel="noopener noreferrer"&gt;always-on OpenClaw hosting on Ampere&lt;/a&gt; can be a hosting option to evaluate when you need a persistent process and repeatable restart boundary, but it does not remove the need to define state persistence, credential scope, health probes, and recovery policy yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to put on the dashboard
&lt;/h2&gt;

&lt;p&gt;Show separate panels for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;process liveness and restart count;&lt;/li&gt;
&lt;li&gt;readiness and admission rejections;&lt;/li&gt;
&lt;li&gt;capability state by dependency and task type;&lt;/li&gt;
&lt;li&gt;probe latency and timeout rate;&lt;/li&gt;
&lt;li&gt;configuration and credential versions;&lt;/li&gt;
&lt;li&gt;unknown outcomes awaiting reconciliation;&lt;/li&gt;
&lt;li&gt;recovery actions and their completion status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not a prettier green dashboard. The goal is to make “alive but unable to do useful work” impossible to confuse with “ready to accept this run.”&lt;/p&gt;

&lt;p&gt;If you build agents, add one tool-path probe this week. Then inject one dependency failure while the process stays alive. If the system cannot reject only the affected work, preserve the evidence, and recover the smallest possible domain, the health check is measuring uptime, not health.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>agents</category>
      <category>observability</category>
    </item>
    <item>
      <title>Your AI Agent Needs a Cancellation Contract, Not Just a Stop Button</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Wed, 19 Aug 2026 19:43:13 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agent-needs-a-cancellation-contract-not-just-a-stop-button-4k8n</link>
      <guid>https://dev.to/zira125/your-ai-agent-needs-a-cancellation-contract-not-just-a-stop-button-4k8n</guid>
      <description>&lt;p&gt;A stop button is not a cancellation protocol.&lt;/p&gt;

&lt;p&gt;In a toy agent, “stop” can mean setting a boolean and waiting for the loop to exit. In a real agent, work may already be queued, claimed by another worker, inside a browser session, or waiting for an outbound side effect. If cancellation is not represented as durable state, a restart can resurrect work the operator thought they stopped.&lt;/p&gt;

&lt;p&gt;The useful question is not “did the process receive SIGTERM?” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can every layer prove whether this run may still start work, whether in-flight work must finish, and what happened to side effects that were interrupted?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article turns cancellation into a small contract you can test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define cancellation as a state machine
&lt;/h2&gt;

&lt;p&gt;Keep cancellation separate from process liveness. A worker can be alive while its run is cancelled, and a worker can die before it records the cancellation.&lt;/p&gt;

&lt;p&gt;A minimal run state machine is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ACTIVE&lt;/strong&gt;: new work may be admitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CANCELLING&lt;/strong&gt;: no new work may start; in-flight work is being observed or stopped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CANCELLED&lt;/strong&gt;: the run will not resume and no unclaimed step may dispatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;COMPLETED&lt;/strong&gt;: the intended work finished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UNKNOWN&lt;/strong&gt;: the controller cannot prove whether an external effect happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store the state durably with a monotonically increasing cancel_version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_id              status       cancel_version  updated_at
run_42              CANCELLING   3               2026-08-19T12:00:00Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers must carry the version they observed. A dispatch is valid only if the durable row still says ACTIVE with the same version. This closes the race where an operator clicks Stop after a worker checked the run but before it starts a tool call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make cancellation explicit at every boundary
&lt;/h2&gt;

&lt;p&gt;A cancellation check only at the top of the agent loop is too weak. Check the contract at each boundary that can create work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Admission&lt;/strong&gt;: reject newly submitted steps for a cancelled run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue claim&lt;/strong&gt;: do not claim a step whose run is CANCELLING or CANCELLED.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool dispatch&lt;/strong&gt;: atomically recheck run state, cancellation version, policy, and credential lease.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry scheduling&lt;/strong&gt;: cancellation revokes future retries, not just the current attempt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser actions&lt;/strong&gt;: stop before the next navigation or mutation, but record that a page action may already be in flight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound delivery&lt;/strong&gt;: apply the delivery policy independently from execution cancellation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last distinction matters. Cancelling a code-generation run does not automatically prove that an already-created notification was unsent. Execution and delivery need separate records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use cooperative and forced cancellation together
&lt;/h2&gt;

&lt;p&gt;Cooperative cancellation is the default: the worker notices the state change at safe checkpoints and exits cleanly. Forced cancellation is a deadline for the worker that does not cooperate.&lt;/p&gt;

&lt;p&gt;A practical sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ACTIVE
  -&amp;gt; CANCELLING (revoke admission and retries)
  -&amp;gt; drain safe checkpoints
  -&amp;gt; CANCELLED (if no in-flight effects remain)
  -&amp;gt; UNKNOWN (if an external effect cannot be reconciled)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not mark a run CANCELLED merely because the worker process exited. A process can die after sending a request and before recording the response. For every external side effect, record an intent with a stable key before dispatch, then reconcile UNKNOWN using the provider’s lookup API, webhook, or an operator decision.&lt;/p&gt;

&lt;p&gt;A cancellation timeout should transition the run to UNKNOWN or CANCELLING_TIMEOUT, not silently to success or cancellation. That makes the ambiguity visible instead of converting it into duplicate work on restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small implementation sketch
&lt;/h2&gt;

&lt;p&gt;The critical operation is a compare-and-set, not a read followed by a write:&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;runs&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CANCELLING'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cancel_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cancel_version&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="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;run_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;run_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACTIVE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A worker dispatch can then require the exact version it observed:&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;steps&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DISPATCHED'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatch_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;cancel_version&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;step_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;step_id&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CLAIMED'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;run_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;run_id&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACTIVE'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cancel_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;cancel_version&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the update affects zero rows, the worker must not call the tool. It should release the claim and record CANCELLED_BEFORE_DISPATCH.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the races, not just the button
&lt;/h2&gt;

&lt;p&gt;A cancellation feature is incomplete until it survives these injected failures:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Expected evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cancel between queue claim and dispatch&lt;/td&gt;
&lt;td&gt;No tool request, or a reconciled effect record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker pauses after the state check&lt;/td&gt;
&lt;td&gt;Stale version is rejected at dispatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process dies after provider request&lt;/td&gt;
&lt;td&gt;Effect becomes UNKNOWN, then reconciles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry timer fires after cancellation&lt;/td&gt;
&lt;td&gt;Retry is rejected and recorded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancellation store is unavailable&lt;/td&gt;
&lt;td&gt;Fail closed for new effects; preserve the run as unresolved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser action is mid-flight&lt;/td&gt;
&lt;td&gt;No next mutation; current action is explicitly unresolved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controller restarts during drain&lt;/td&gt;
&lt;td&gt;Durable CANCELLING state resumes the drain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For each case, assert both safety and evidence: no unauthorized new effect, no lost cancellation intent, and a record an operator can explain later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting does not define cancellation semantics
&lt;/h2&gt;

&lt;p&gt;If you run an always-on OpenClaw or browser agent, a managed runtime such as &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=cancellation-contract" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; can be one deployment option to evaluate. It does not replace durable run state, fencing, credential scope, or reconciliation. Those remain properties of the agent control plane.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before trusting a Stop button, verify that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cancellation is durable and versioned;&lt;/li&gt;
&lt;li&gt;admission, claim, dispatch, retry, and delivery each recheck it;&lt;/li&gt;
&lt;li&gt;workers cannot dispatch with a stale cancellation version;&lt;/li&gt;
&lt;li&gt;cooperative drain has a bounded forced-cancellation path;&lt;/li&gt;
&lt;li&gt;external effects have stable keys and an UNKNOWN outcome;&lt;/li&gt;
&lt;li&gt;restart resumes from durable cancellation state;&lt;/li&gt;
&lt;li&gt;tests inject races at every side-effect boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical goal is not instant termination. It is a system that can prove what was prevented, what was already in flight, and what still needs reconciliation. That is the difference between a UI button and an operational cancellation contract.&lt;/p&gt;

&lt;p&gt;If you are building coding agents or automation that must survive restarts and operator intervention, follow for more concrete control-plane tests and failure drills.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>agents</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your AI Agent's Deadline Is Not a Timeout Until Every Worker Shares It</title>
      <dc:creator>Zira</dc:creator>
      <pubDate>Wed, 19 Aug 2026 15:43:38 +0000</pubDate>
      <link>https://dev.to/zira125/your-ai-agents-deadline-is-not-a-timeout-until-every-worker-shares-it-4ao</link>
      <guid>https://dev.to/zira125/your-ai-agents-deadline-is-not-a-timeout-until-every-worker-shares-it-4ao</guid>
      <description>&lt;p&gt;A timeout is local. A deadline is a contract.&lt;/p&gt;

&lt;p&gt;That distinction matters once an AI agent fans work out to queues, workers, browser sessions, MCP servers, or child agents. If every hop invents its own timeout, the system can keep doing work after the user-visible request has already expired. Worse, a retry may outlive the approval, credential lease, or business deadline that made the original call valid.&lt;/p&gt;

&lt;p&gt;The practical fix is to propagate one absolute deadline through the execution graph and enforce it at every boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;p&gt;Suppose a request starts at 12:00:00 with a 30-second budget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The orchestrator waits 30 seconds.&lt;/li&gt;
&lt;li&gt;A queue adds 5 seconds of delay.&lt;/li&gt;
&lt;li&gt;A worker starts with its own 30-second timeout.&lt;/li&gt;
&lt;li&gt;A browser tool retries twice with 10 seconds each.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user sees a timeout at 12:00:30, but the worker and browser can continue until 12:01:05. That is not just wasted compute. A late tool call can send a message, mutate a record, or use a credential after the request is no longer authorized.&lt;/p&gt;

&lt;p&gt;A local timeout answers how long will this process wait? A propagated deadline answers after what instant must every hop stop starting new work?&lt;/p&gt;

&lt;h2&gt;
  
  
  Use an absolute deadline, not a chain of durations
&lt;/h2&gt;

&lt;p&gt;At ingress, calculate one deadline using a monotonic clock. Pass the deadline, rather than the remaining duration, to every child operation.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from dataclasses import dataclass
import time

@dataclass(frozen=True)
class RunContext:
    run_id: str
    deadline_ns: int
    policy_version: str
    credential_version: str

def remaining_seconds(ctx: RunContext) -&amp;gt; float:
    return max(0.0, (ctx.deadline_ns - time.monotonic_ns()) / 1_000_000_000)

def can_start(ctx: RunContext, reserve_seconds: float = 0.0) -&amp;gt; bool:
    return remaining_seconds(ctx) &amp;gt; reserve_seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The wall-clock timestamp is useful for logs, but elapsed-time decisions should use a monotonic clock. Wall clocks can jump because of NTP corrections, VM suspension, daylight-saving changes, or operator adjustments.&lt;/p&gt;

&lt;p&gt;A child must not replace the parent deadline with a fresh timeout:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def child_budget(ctx: RunContext, expected_seconds: float) -&amp;gt; float:
    remaining = remaining_seconds(ctx)
    return min(expected_seconds, remaining)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If the remaining budget is zero, the child should be rejected before dispatch. Do not start a call and hope the provider notices the deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make deadline checks part of dispatch
&lt;/h2&gt;

&lt;p&gt;Check the contract at each side-effect boundary, not only around the top-level request:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before enqueue: reject work whose deadline is already expired.&lt;/li&gt;
&lt;li&gt;Before claim: a worker must not claim expired work.&lt;/li&gt;
&lt;li&gt;Before tool dispatch: recheck deadline, policy version, approval, and credential version atomically.&lt;/li&gt;
&lt;li&gt;Before retry: calculate the retry reservation, including backoff and provider timeout.&lt;/li&gt;
&lt;li&gt;Before outbound delivery: use a separate delivery deadline when the notification is still useful after execution expires.&lt;/li&gt;
&lt;li&gt;After an ambiguous result: record UNKNOWN and reconcile with the provider instead of blindly retrying.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fourth check is commonly missed. A retry with a 2-second provider timeout is unsafe when only 500 milliseconds remain. Backoff is work too, and a retry reservation should include it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate execution and delivery deadlines
&lt;/h2&gt;

&lt;p&gt;A user request can expire while its completion notification is still worth sending. Model those as separate contracts:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "execution_deadline": "monotonic:...",
  "delivery_deadline": "monotonic:...",
  "max_attempts": 2,
  "effect_key": "run-123:completion:v1"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Execution expiry means do not begin another tool or model step. Delivery expiry means do not send this notification after it is stale. Keeping the ledgers separate prevents a failed notification from causing the business action to run again.&lt;/p&gt;

&lt;p&gt;For a hosted, always-on OpenClaw runtime, &lt;a href="https://ampere.sh/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=deadline-propagation" rel="noopener noreferrer"&gt;managed OpenClaw hosting on Ampere&lt;/a&gt; is one deployment option to evaluate. It does not define deadline semantics, cancel already-dispatched side effects, or remove the need for idempotency and credential checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test clock and queue failures deliberately
&lt;/h2&gt;

&lt;p&gt;A useful test matrix should include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queue delay consumes the full budget&lt;/td&gt;
&lt;td&gt;Worker refuses to claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker clock is ahead&lt;/td&gt;
&lt;td&gt;Monotonic deadline still governs local enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wall clock jumps backward&lt;/td&gt;
&lt;td&gt;No extra execution time is created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry reservation exceeds remaining budget&lt;/td&gt;
&lt;td&gt;Retry is rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deadline expires during provider call&lt;/td&gt;
&lt;td&gt;Outcome is recorded, not guessed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution expires but delivery remains valid&lt;/td&gt;
&lt;td&gt;Completion is delivered once, if still useful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker restarts after expiry&lt;/td&gt;
&lt;td&gt;Recovery does not resurrect the run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate dispatch races with expiry&lt;/td&gt;
&lt;td&gt;One effect key and provider reconciliation decide the result&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Inject delays at enqueue, claim, pre-dispatch, provider response, and delivery. Record the deadline, remaining budget, clock source, policy version, credential version, and effect key in the evidence trail. Without those fields, a log line saying timed out is not enough to explain whether the work never started, was cancelled, or completed ambiguously.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact review checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping an agent workflow, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One ingress deadline is propagated to every child operation.&lt;/li&gt;
&lt;li&gt;Duration budgets are derived from the deadline, never reset at a hop.&lt;/li&gt;
&lt;li&gt;Monotonic time is used for enforcement and wall time only for display.&lt;/li&gt;
&lt;li&gt;Queue claim and side-effect dispatch both recheck expiry.&lt;/li&gt;
&lt;li&gt;Retries reserve backoff plus execution time.&lt;/li&gt;
&lt;li&gt;Execution and outbound delivery have separate state machines.&lt;/li&gt;
&lt;li&gt;Expired or ambiguous effects use stable idempotency keys and reconciliation.&lt;/li&gt;
&lt;li&gt;Restarts cannot resurrect work past its deadline.&lt;/li&gt;
&lt;li&gt;Tests inject queue delay, clock jumps, provider ambiguity, and duplicate races.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model can choose the next action, but the runtime must decide whether there is still time and authority to take it. A deadline becomes real only when every queue, worker, tool, and delivery path treats it as the same contract.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>testing</category>
      <category>agents</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
