{"slug": "your-ai-agent-checked-the-state-someone-changed-it-before-execution", "title": "Your AI Agent Checked the State. Someone Changed It Before Execution.", "summary": "A developer argues that AI agents executing real actions inherit classic time-of-check-to-time-of-use (TOCTOU) bugs from distributed systems, illustrating how an agent that verifies a refundable balance can act on a stale snapshot after another service has already partially refunded the order. The writeup contends that prompting the model to check more carefully or repeating the same check before execution is insufficient, and that reliable agents need concurrency control, version validation, transactional execution, and carefully designed boundaries between the agent and the systems it operates.", "body_md": "*Time-of-check-to-time-of-use bugs, stale reads, and concurrent updates are not just distributed-systems problems anymore. When AI agents can execute real actions, they become authorisation and safety problems.*\n\nThe AI agent did everything it was supposed to do.\n\nA customer asked for a refund. The agent retrieved the order, checked its status, verified the refundable balance, evaluated the applicable policy, and determined that the request was allowed.\n\nThe order was eligible. The amount was correct. The customer owned the order.\n\nThe agent proceeded. Except that another service had already refunded part of the order.\n\nThe balance the agent checked was no longer the balance that existed when the refund reached the payment provider. Depending on how the application was designed, the customer might receive too much money, the transaction might fail, or the system might record a result that does not match reality.\n\nNothing was necessarily wrong with the agent's reasoning. Its information was simply no longer current.\n\nThis is a classic time-of-check-to-time-of-use problem, commonly abbreviated as TOCTOU. A system checks whether an operation is permitted, assumes the relevant conditions will remain unchanged, and then performs the operation after those conditions have changed.\n\nTraditional software has dealt with this problem for decades. AI agents make it more interesting because they introduce additional planning steps, tool calls, context changes, retries, and decisions between the initial check and the final action.\n\nAn agent might inspect an order, call another API, summarise a document, wait for human approval, and then attempt the refund. Every additional step can widen the window in which the underlying state changes.\n\nThe solution is not to tell the model to check more carefully. Nor is it sufficient to repeat the same check immediately before execution and assume the problem is solved.\n\nThe real question is whether the system can guarantee that the conditions used to authorise an action still hold when the action takes effect.\n\nThat requires a combination of concurrency control, version validation, transactional execution, and carefully designed boundaries between the AI agent and the systems it operates.\n\nImagine a conventional application that allows a customer to withdraw money from an account.\n\nThe account has a balance of ₹10,000. Two requests arrive almost simultaneously, each attempting to withdraw ₹7,000.\n\nBoth requests read the balance before either withdrawal is committed.\n\n```\nInitial balance: ₹10,000\n\nRequest A reads balance: ₹10,000\nRequest B reads balance: ₹10,000\n\nRequest A checks: ₹7,000 <= ₹10,000\nRequest B checks: ₹7,000 <= ₹10,000\n\nRequest A withdraws ₹7,000\nRequest B withdraws ₹7,000\n\nExpected outcome: One withdrawal succeeds.\nPossible outcome with unsafe implementation: Both succeed.\n```\n\nThe problem is not that either request calculated incorrectly. Both made a decision using the same earlier state.\n\nThe application treated a read as though it reserved the state for future use.\n\nIt did not.\n\nNow introduce an AI agent.\n\nThe agent retrieves the account balance, asks a policy engine whether the withdrawal is permitted, plans the next step, and calls a tool. Perhaps it also waits for approval. The same concurrency problem remains, but the decision window can become substantially longer.\n\nAn AI agent does not need to be malicious, confused, or poorly prompted to trigger this failure. It can make a perfectly reasonable decision from a snapshot that has already become invalid.\n\nThis is why I think production AI reliability needs to borrow more heavily from distributed-systems engineering. Once an agent operates on mutable business state, correct reasoning is only part of the problem. The system must also preserve the validity of the assumptions on which that reasoning depends.\n\nTOCTOU describes a gap between checking a condition and using the result of that check.\n\nConsider this sequence:\n\n```\nT1: Read the resource\nT2: Check permissions and business rules\nT3: Prepare the action\nT4: Resource changes\nT5: Execute the action using the old decision\n```\n\nThe dangerous interval is between T2 and T5.\n\nAt T2, the operation might genuinely be permitted. At T5, it might no longer be permitted.\n\nThe important distinction is between **the validity of a decision at check time and the validity of that decision at execution time**.\n\nA check proves something about the state it observed. It does not automatically reserve that state, lock it, or guarantee that another process cannot change it.\n\nFor example, an agent may check that:\n\nAnother process can change any of these conditions before the agent acts.\n\nTOCTOU is especially important when the action is irreversible or has external consequences. A stale product recommendation is inconvenient. A stale permission check followed by a financial transaction can be a security incident.\n\nThere is a subtle but important point here: not every state change invalidates every decision. If an unrelated field changes, the original action may remain safe. The engineering task is to identify the exact invariants that must hold when the operation takes effect and enforce them at the appropriate boundary.\n\nTraditional applications often execute a relatively short sequence of deterministic operations. AI agents introduce workflows in which the duration and number of intermediate steps are less predictable.\n\nConsider an agent handling a refund:\n\n```\n1. Receive customer request.\n2. Retrieve order details.\n3. Retrieve payment history.\n4. Check refund policy.\n5. Estimate the refundable balance.\n6. Prepare a proposed action.\n7. Request human approval.\n8. Receive approval.\n9. Call the payment API.\n10. Record the result.\n```\n\nBetween steps 2 and 9, another customer-support agent, a scheduled job, a payment webhook, or a human operator may update the same order.\n\nThe agent may also call additional tools because its plan changes. A tool response may introduce new information, or an execution attempt may time out and require recovery.\n\nEvery additional operation introduces a possible delay, and every delay gives concurrent processes more opportunities to change the underlying state.\n\nI see four characteristics that make this particularly relevant to AI systems.\n\n**First, planning is not atomic.** The agent's reasoning process may involve several model calls and tool interactions. The business state is not frozen while the agent reasons.\n\n**Second, context can become stale.** A tool response retrieved earlier may remain in the agent's context even after the underlying resource changes.\n\n**Third, retries can repeat old assumptions.** A failed execution attempt may be retried using the same plan without checking whether the original conditions still hold.\n\n**Fourth, approval adds another boundary.** A human may approve an action based on one version of a resource, while the execution service later encounters a different version.\n\nThe common mistake is to treat the agent's plan as though it were a transaction.\n\nIt is not.\n\nA plan describes what the agent intends to do. It does not guarantee that the conditions required to do it still exist.\n\nLet's look at a more realistic scenario.\n\nAn order has a refundable balance of ₹10,000. Two support agents independently handle separate requests related to that order.\n\nAgent A proposes a refund of ₹7,000.\n\nAgent B proposes a refund of ₹6,000.\n\nEach agent retrieves the same order state. Each verifies the refundable balance. Each asks the policy engine whether its proposed amount is within the limit.\n\nBoth requests pass their checks.\n\nThe problem becomes clear when we look at the interleaving of events.\n\n| Time | Agent A | Agent B | \n|---|---|---|\n| T1 | Reads balance: ₹10,000 |  | \n| T2 |  | Reads balance: ₹10,000 | \n| T3 | Validates ₹7,000 refund |  | \n| T4 |  | Validates ₹6,000 refund | \n| T5 | Executes ₹7,000 refund |  | \n| T6 |  | Executes ₹6,000 refund using stale balance | \n\nThe combined refund is ₹13,000, even though only ₹10,000 was refundable.\n\nBoth agents followed the same policy. The policy itself may be perfectly correct. The failure occurred because the system did not enforce the shared invariant at the point where concurrent operations changed the state.\n\nThe invariant is:\n\n[\n\n\\text{Total refunds} \\leq \\text{Refundable limit}\n\n]\n\nA pre-execution check cannot guarantee this invariant when another request can change the balance after the check.\n\nThe application must make the state transition itself conditional on the invariant still holding.\n\nThat is the difference between checking whether an operation is allowed and making sure that only an allowed operation can commit.\n\nA common first response is to repeat the validation immediately before calling the provider.\n\nThat is better than relying entirely on an old read, but it does not necessarily solve TOCTOU.\n\nConsider the following sequence:\n\n```\norder = get_order(order_id)\n\nif order.refundable_balance_minor >= amount_minor:\n    execute_refund(order_id, amount_minor)\n```\n\nThe application checks the balance and then calls the provider.\n\nWhat if another worker changes the balance after the condition evaluates but before `execute_refund` takes effect?\n\nThe same race still exists.\n\nMoving the check closer to execution narrows the window. It does not eliminate it.\n\nThe correct solution depends on where the authoritative state lives and whether the check and state change can be made atomic.\n\nIf the balance is controlled by your own database, you may be able to enforce the invariant inside a transaction or through a conditional update.\n\nIf the balance is controlled by an external payment provider, your local transaction cannot automatically make the provider's operation atomic with your own database. You need provider-supported concurrency controls, idempotency, reconciliation, or another design that handles the distributed boundary explicitly.\n\nThis distinction matters because a system can have excellent local transaction handling and still produce incorrect external effects if it assumes that a local check reserves a remote resource.\n\nOne useful technique is optimistic concurrency control.\n\nInstead of locking a resource throughout a potentially long workflow, the application reads the resource with a version number and checks that the version has not changed when it attempts to update the resource.\n\nSuppose the order record contains:\n\n```\n{\n  \"order_id\": \"ORD-1042\",\n  \"refundable_balance_minor\": 1000000,\n  \"version\": 12\n}\n```\n\nThe agent reads version 12 and prepares a refund.\n\nBefore it commits the update, another worker modifies the order. The version becomes 13.\n\nThe first worker then attempts an update using version 12. Because the current version is 13, the update fails.\n\nThe application knows that its decision was based on a stale state.\n\nA simplified SQL example looks like this:\n\n```\nUPDATE orders\nSET refundable_balance_minor =\n        refundable_balance_minor - :amount_minor,\n    version = version + 1\nWHERE order_id = :order_id\n  AND tenant_id = :tenant_id\n  AND version = :expected_version\n  AND refundable_balance_minor >= :amount_minor;\n```\n\nThe application checks the affected row count.\n\nIf exactly one row was updated, the conditional state transition succeeded. If zero rows were updated, the application must not assume the refund was reserved.\n\nPossible explanations include:\n\nThe application should distinguish these outcomes where necessary, without weakening the atomic condition.\n\nThe important part is that the version check and state update happen in the same database statement.\n\nThis is not equivalent to reading the version in one query, comparing it in application code, and issuing an unconditional update afterwards. That would reintroduce the race.\n\nOptimistic locking works well when conflicts are relatively uncommon, and the application can retry or request a fresh decision when a conflict occurs.\n\nIt is also useful for AI agents because the agent can perform its planning work without holding a database lock open for the duration of a model call.\n\nHowever, there is an important limitation: optimistic locking only protects the state covered by the version check and conditional operation. It does not automatically protect unrelated records, external services, or resources that do not participate in the same concurrency protocol.\n\nWhen optimistic locking detects a conflict, the application has a choice.\n\nIt can reject the action, retrieve a fresh state, or attempt to rebuild the operation under a defined policy.\n\nWhat it should not do is blindly retry the same stale plan until the update succeeds.\n\nSuppose the agent originally planned to refund ₹7,000 from a ₹10,000 balance. Another process refunds ₹6,000, leaving only ₹4,000.\n\nThe version check correctly rejects the original ₹7,000 refund.\n\nA dangerous retry loop might simply fetch the new version and retry the same amount. The update will continue to fail because the invariant no longer holds. A worse implementation might drop the balance condition and force the operation through.\n\nA safer workflow is:\n\n```\nConflict detected\n       |\n       v\nDiscard stale execution decision\n       |\n       v\nRetrieve the current authoritative state\n       |\n       v\nReevaluate business rules and policy\n       |\n       +----> No longer eligible: Reject\n       |\n       +----> Material change: Request reapproval\n       |\n       +----> Still eligible: Build a new action\n                            |\n                            v\n                  Validate and execute safely\n```\n\nThe distinction between rebuilding an action and retrying an operation is important.\n\nA transport retry attempts to deliver the same logical operation again. Replanning creates a new decision based on new information.\n\nThe two should not share the same assumptions.\n\nIf the relevant state changes materially after a human approves an action, the application should determine whether the original approval remains valid. A change to the remaining refundable balance might require a new approval because the risk or outcome has changed.\n\nAn AI agent can help interpret the new state and propose an alternative. It should not be allowed to silently transform a rejected action into a different authorised action.\n\nOptimistic locking detects unexpected changes. Database transactions can provide stronger guarantees when the relevant operations are within the same transactional system.\n\nSuppose the application maintains the authoritative refundable balance in its own database. It can reserve or deduct the amount as part of a transaction, provided the data model and business process support that design.\n\nA simplified example:\n\n```\nBEGIN;\n\nUPDATE orders\nSET refundable_balance_minor =\n        refundable_balance_minor - :amount_minor,\n    version = version + 1\nWHERE order_id = :order_id\n  AND tenant_id = :tenant_id\n  AND refundable_balance_minor >= :amount_minor;\n\n-- Application must verify that exactly one row was updated.\n-- If not, roll back.\n\nINSERT INTO refund_operations (\n    operation_id,\n    order_id,\n    amount_minor,\n    status\n)\nVALUES (\n    :operation_id,\n    :order_id,\n    :amount_minor,\n    'PENDING'\n);\n\nCOMMIT;\n```\n\nThis example assumes that the application has already validated the action's identity and currency and that the database schema enforces appropriate constraints.\n\nIt also omits implementation-specific handling for the affected row count. In production, the application must roll back if the conditional update affects zero rows and ensure that the operation record is created only when the reservation succeeds.\n\nThe transaction protects the local state transition: the balance update and operation record either commit together or roll back together.\n\nThat gives the application a durable record that the amount has been reserved for a particular logical operation.\n\nHowever, this is not yet the same as completing a refund with an external payment provider.\n\nThe provider call cannot normally be included in the local database transaction. If the application calls the provider and then the database transaction fails, the provider may have executed the refund even though the local transaction did not commit.\n\nSimilarly, if the database commits first and the provider call fails, the operation remains reserved but has not completed externally.\n\nA production system must explicitly handle this boundary.\n\nAI agents frequently operate through external APIs: payment providers, CRM systems, ticketing platforms, cloud infrastructure, email services, and internal microservices.\n\nA local database transaction cannot guarantee atomic execution across all these systems.\n\n```\n1. Reserve refund in the local database.\n2. Commit local transaction.\n3. Call the payment provider.\n4. Provider executes refund.\n5. Network response is lost.\n```\n\nThe application does not know whether the refund succeeded merely because it did not receive a response.\n\nIssuing a second refund request without a recovery strategy may create a duplicate effect.\n\nThis is where durable execution records, idempotency, and reconciliation become essential.\n\nA robust design typically creates a durable operation record with a unique operation ID. A worker processes that operation, uses a stable provider idempotency key when supported, and records the outcome.\n\nIf the worker crashes, another worker can resume processing based on the durable state instead of inventing a new operation.\n\nIf the outcome is uncertain, the system reconciles against the provider before attempting an action that could duplicate the effect.\n\nThe key states might include:\n\n`PENDING`: the operation has been durably created.` IN_PROGRESS`: a worker is processing it.` SUCCEEDED`: the provider's successful outcome is confirmed.` FAILED`: a definitive failure is known.` OUTCOME_UNKNOWN`: the request may have succeeded, but confirmation is missing.`RECONCILIATION_REQUIRED`: automated recovery cannot safely determine the result.\nThe precise state machine depends on the provider and business process. Some failures are retryable; others require manual intervention.\n\nIdempotency keys are particularly important, but they are not magic. Their scope, retention, request-matching rules, and behavior after timeouts depend on the provider's contract. The application must use them according to those semantics and retain enough information to recover safely.\n\nA stable key should identify the same logical operation across retries. Generating a new key for every retry can defeat the intended protection.\n\nOne way to coordinate local state changes with asynchronous external execution is the transactional outbox pattern.\n\nInstead of updating the business record and then attempting to publish a message separately, the application commits the state change and an outbox event in the same database transaction.\n\nFor example:\n\n```\nBEGIN;\n\nUPDATE orders\nSET refundable_balance_minor =\n        refundable_balance_minor - :amount_minor,\n    version = version + 1\nWHERE order_id = :order_id\n  AND tenant_id = :tenant_id\n  AND refundable_balance_minor >= :amount_minor;\n\n-- Verify that exactly one row was updated.\n\nINSERT INTO refund_operations (\n    operation_id,\n    order_id,\n    amount_minor,\n    status\n)\nVALUES (\n    :operation_id,\n    :order_id,\n    :amount_minor,\n    'PENDING'\n);\n\nINSERT INTO outbox_events (\n    event_id,\n    event_type,\n    aggregate_id,\n    payload\n)\nVALUES (\n    :event_id,\n    'RefundRequested',\n    :order_id,\n    :event_payload\n);\n\nCOMMIT;\n```\n\nA separate worker reads the outbox and sends the request to the payment provider.\n\nThe advantage is that the business state and the intention to perform the external operation are recorded together. If the application crashes after the transaction commits, the outbox event remains available for processing.\n\nThe worker must still handle duplicate delivery, provider idempotency, and uncertain outcomes. An outbox does not guarantee exactly-once delivery to an external API. It helps make the handoff durable and recoverable.\n\nThis pattern is especially useful for AI agents because it separates the model's planning lifecycle from the execution lifecycle.\n\nThe model can propose an action, the application can validate and commit an authorized operation, and a worker can execute it reliably without requiring the model to remain active throughout the transaction.\n\nTOCTOU is not limited to business data. Authorisation itself can become stale.\n\nSuppose an AI agent checks that a user has permission to delete a production resource. The policy engine returns `ALLOW`.\n\nBefore the delete operation executes, an administrator revokes that user's access.\n\nIf the application relies solely on the earlier decision, it may execute an action that is no longer authorised.\n\nThis is a time-of-check-to-time-of-use problem involving permissions rather than balances.\n\nA safer execution path checks the relevant authorisation conditions immediately before the action takes effect.\n\nDepending on the system, this may involve:\n\nA policy version can be useful if the application defines a clear invalidation model. For example, an authorisation decision might be associated with version 42 of a policy. If the relevant policy changes to version 43, execution can require a fresh decision.\n\nHowever, version checks alone do not solve every authorisation race. The resource-owning service must enforce the actual permission, and the system must define how revocation interacts with in-flight operations.\n\nFor highly sensitive actions, the application may need a linearizable authorization check, a short-lived capability bound to a specific action, or a design in which the resource owner validates authorization as part of the state-changing operation.\n\nThe objective is to avoid treating an old `ALLOW` response as a permanent permission.\n\nThere is a temptation to solve stale state by asking the model to retrieve everything again before every action.\n\nThat may reduce some failures, but it is not a sufficient concurrency strategy.\n\nThe agent cannot guarantee that the state will remain unchanged after the final retrieval. It also cannot reliably coordinate concurrent updates across services merely by reasoning about them.\n\nInstead, give the agent a clear execution contract.\n\nThe agent can:\n\nThe execution service must:\n\nThis separation is important because a model-generated plan is not a lock, a reservation, or a transaction.\n\nA useful response protocol might distinguish:\n\n```\nALLOW\n    Action is currently eligible for execution.\n\nCONFLICT\n    Relevant state changed; refresh and reevaluate.\n\nDENY\n    Current policy prohibits the action.\n\nAPPROVAL_REQUIRED\n    Human authorisation is required before execution.\n\nOUTCOME_UNKNOWN\n    Execution may have occurred; reconcile before retrying.\n```\n\nThese outcomes let the agent respond appropriately without giving it authority to override the decision.\n\nA `CONFLICT` response should not automatically trigger repeated retries of the same payload. The application should determine whether a new plan is permissible, whether human reapproval is required, or whether the action must be abandoned.\n\nThis approach makes the agent a participant in a controlled workflow rather than the component responsible for maintaining consistency across the entire system.\n\nConcurrency defects are difficult to catch with ordinary happy-path tests because they depend on timing and interleaving.\n\nA test that executes one refund at a time may never expose the bug.\n\nThe test suite should deliberately create competing operations and force changes between checking and execution.\n\nHere are several scenarios worth testing.\n\n**Two refunds against one balance**\n\nStart two requests simultaneously, each of which would be valid against the initial balance but invalid when combined. Verify that the total successful refund amount never exceeds the permitted limit.\n\n**State changes after approval**\n\nApprove an action, change a relevant resource field, and then attempt execution. Verify that the old action is rejected or subjected to the explicitly defined revalidation process.\n\n**Permission revocation**\n\nGrant access, obtain an initial authorisation decision, revoke access, and then attempt the action. Verify the intended revocation semantics.\n\n**Concurrent version updates**\n\nHave two workers read the same resource version and attempt updates. Verify that at most one update using that version succeeds and that the losing worker handles the conflict correctly.\n\n**Provider timeout after execution**\n\nSimulate a provider that performs the operation but drops the response. Verify that recovery does not create a duplicate effect.\n\n**Worker crash after reservation**\n\nCommit a local reservation and operation record, then crash before sending the provider request. Verify that another worker can recover the operation safely.\n\n**Outbox duplicate delivery**\n\nDeliver the same event more than once. Verify that the consumer and provider integration handles duplicates according to the operation's idempotency rules.\n\n**Stale model context**\n\nLet the agent retrieve a resource, change the underlying state, and then submit the old plan. Verify that the execution service—not the model—detects the conflict.\n\nFor database-backed workflows, use concurrent integration tests against the actual database engine and isolation configuration. Mocked tests are useful for individual components, but they cannot prove that the real conditional update, transaction, or locking behaviour is correct.\n\nProperty-based testing can also help. Generate competing operations and verify invariants such as:\n\n[\n\n\\text{Successful refunds} \\leq \\text{Authorized refundable limit}\n\n]\n\nThe test should examine committed outcomes, not just the decisions made by the agents.\n\nBefore deploying an AI agent that modifies shared or externally managed state, ask the following questions.\n\nIf the system cannot answer these questions, it may be relying on the assumption that the state remains stable while the agent thinks.\n\nThat assumption is rarely safe in a distributed system.\n\nAn AI agent can make the correct decision and still cause an incorrect outcome.\n\nIt can inspect a valid resource, evaluate a legitimate policy, and construct an appropriate action. Another process can then change the resource before execution.\n\nThe agent's reasoning was correct for the state it observed. The system failed to preserve the conditions required for that decision to remain valid.\n\nThis is why reliable AI agents need more than better prompts, stronger models, and additional validation calls. They need the same concurrency guarantees that reliable transactional software has always required.\n\nUse optimistic locking to detect stale versions. Use conditional updates and transactions to enforce invariants within a database. Use durable operation records, idempotency, and reconciliation to manage external effects. Revalidate authorisation at the appropriate execution boundary. Treat conflicts as reasons to reevaluate, not invitations to retry blindly.\n\nAnd above all, separate the agent's intention from the system's guarantee that the intended action can safely take effect.\n\nThe model can decide what it wants to do next. It cannot guarantee that the world still looks the way it did when that decision was made.\n\n**A state check tells you what was true. Concurrency control determines whether it is still safe to act on that truth.**", "url": "https://wpnews.pro/news/your-ai-agent-checked-the-state-someone-changed-it-before-execution", "canonical_source": "https://dev.to/katul1512/your-ai-agent-checked-the-state-someone-changed-it-before-execution-29p9", "published_at": "2026-10-29 05:36:40+00:00", "updated_at": "2026-10-29 05:43:39.840904+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "artificial-intelligence"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/your-ai-agent-checked-the-state-someone-changed-it-before-execution", "markdown": "https://wpnews.pro/news/your-ai-agent-checked-the-state-someone-changed-it-before-execution.md", "text": "https://wpnews.pro/news/your-ai-agent-checked-the-state-someone-changed-it-before-execution.txt", "jsonld": "https://wpnews.pro/news/your-ai-agent-checked-the-state-someone-changed-it-before-execution.jsonld"}}