An agent has 30 seconds to finish a request. After 20 seconds of work, the host’s wall clock moves backwards by two minutes. The adapter subtracts the current timestamp from its stored deadline and gives the next tool call 130 seconds.

That is the behaviour of a small, deliberately faulty calculation. It is not a reported incident in Claude Code or an MCP product. You can reproduce it without changing your computer’s clock.

My rule for a local agent runtime is to create one monotonic deadline when the request enters the bounded workflow. Every queue wait, tool attempt and backoff spends that same budget. UTC timestamps still belong in the incident record, but they should not calculate the remaining local elapsed-time allowance.

Reproduce the extra time

Download the Python fixture, inspect it, and run it with Python 3.9 or later:

python3 agent-monotonic-deadline.py
python3 agent-monotonic-deadline.py --test
python3 -O agent-monotonic-deadline.py --test

The script uses only the standard library. It makes no network requests, changes no host clocks and runs no model. Its injected clock advances immediately rather than making you wait.

after 20s and wall clock -120s:
wall remaining: 130 seconds
monotonic remaining: 10 seconds
at 30s: 0 seconds remain
retry after 12s work + 4s backoff: 14.0 seconds

The vulnerable calculation is wall_deadline - current_wall_time. In the fixture, the wall clock begins at 1,000 seconds and the deadline is 1,030. Twenty seconds later, a negative 120-second adjustment makes the wall clock read 900. The subtraction returns 130.

Python documents time.time() as seconds since the epoch and explicitly allows a lower return value than an earlier call if the system clock has been set backwards. time.monotonic() cannot go backwards and is unaffected by system clock updates. Its reference point is undefined; differences between readings are what you can use.

This is about clock correction, not changing a displayed time zone. Switching a dashboard from London to New York does not itself change epoch time.

Keep the deadline outside the retry loop

The fixture’s Deadline class defaults to time.monotonic_ns. Integer nanoseconds keep the arithmetic explicit. The adapter converts the remaining allowance to seconds only when handing it to a timeout interface.

class Deadline:
    def __init__(self, budget_ns, clock=time.monotonic_ns):
        if type(budget_ns) is not int or budget_ns <= 0:
            raise ValueError('positive integer nanoseconds required')
        self.clock = clock
        self.end = clock() + budget_ns

    def remaining_ns(self):
        return max(0, self.end - self.clock())

    def timeout_seconds(self, cap_ns):
        if type(cap_ns) is not int or cap_ns <= 0:
            raise ValueError('positive integer cap required')
        remaining = self.remaining_ns()
        if remaining == 0:
            raise TimeoutError('run deadline exhausted')
        return min(cap_ns, remaining) / NS

The download includes the imports, NS = 1_000_000_000, the synthetic clock and tests. This class computes an allowance. It does not execute or interrupt a tool.

Create the object once, before entering the queue if queue time belongs to the request objective. Suppose an attempt consumes 12 seconds and its backoff consumes another four. The next attempt can receive at most 14 seconds, even if its usual per-attempt cap is 30. Creating a new Deadline(30 * NS) inside the retry loop grants a new allowance each time.

The two controls answer different questions. A per-attempt cap limits how long one attempt may occupy the adapter. The shared deadline limits the local workflow. You may need both, alongside the attempt accounting that prevents retry amplification.

Read the remaining allowance again after backoff and after acquiring a concurrency permit. A value computed before a long wait is stale by the time the tool starts. Do not pass zero to a library without checking its contract: an interface may interpret zero differently from your application’s meaning of “expired”. The fixture raises before dispatch when the budget is exhausted.

What the tests prove

The fixture has eleven distinct unit tests. They cover a backwards wall-clock jump, a forwards jump, exact expiry, overdue clamping, the per-attempt cap, retry plus backoff accounting, queue wait accounting, accidental deadline reset, invalid budgets, invalid caps and the default clock binding.

The reset test deliberately demonstrates the wrong alternative. At elapsed second 29, the original deadline has one second left. A newly constructed deadline has 30. Passing that test means the counterexample was reproduced, not that resetting the deadline is safe.

The numeric input tests reject booleans, nonpositive values and floating-point budgets, including NaN and infinity. This is a small internal API with an integer contract. A production configuration loader still needs a documented unit, an upper bound and validation before converting a user-supplied duration.

These are deterministic component tests of deadline arithmetic. They do not establish an operating-system scheduling bound, network cancellation behaviour or an agent platform’s reliability. The synthetic clock does not measure a real NTP correction. It lets the same failure sequence run on every test invocation.

A timeout does not stop an external effect

A correct clock cannot make an uncooperative operation cancellable. If an HTTP client applies its timeout separately to connection and read phases, that may differ from the total deadline you intended. Inspect the actual client’s contract and test a slow response, a stalled connection and cancellation during a write.

For an asyncio adapter, Python’s timeout documentation gives an absolute-deadline interface, asyncio.timeout_at, based on the event loop’s clock. Derive that deadline from loop.time() and stay in that clock domain. Do not mix an epoch timestamp into it. This API requires Python 3.11 or later; the downloadable arithmetic fixture does not use it.

The same documentation warns that wait_for waits for cancellation to complete, so total wait time can exceed the requested timeout. Blocking work can also prevent the event loop from handling cancellation promptly. A timeout on an await does not prove that a remote server stopped processing an already accepted request.

For writes, record an unknown outcome until reconciliation establishes what happened. Do not automatically retry a charge or deployment because your local deadline expired. An idempotency contract and an authoritative effect lookup are separate engineering work; a shorter timeout cannot replace them.

Define the boundary when work moves

Keep this helper within a live clock domain. Do not serialize its numeric end value into a portable job record and assume another host or a reboot can interpret it. The helper has no restart protocol, clock identity or remote deadline negotiation.

For cross-service work, choose a documented deadline-propagation mechanism that accounts for transmission delay and clock differences. Giving every receiver the original full duration extends the overall request. Giving a receiver the sender’s remaining duration without accounting for transit can also extend it. The local fixture intentionally leaves distributed enforcement unimplemented.

Suspend behaviour needs an explicit policy too. A monotonic clock’s treatment of machine suspend depends on the platform and clock. If “30 seconds” must include time while a laptop is asleep, test that requirement on the target platform rather than assuming this helper proves it.

Permission expiry is another boundary. A token’s UTC expiration and a local elapsed-time timeout have different meanings. Keep the issuer’s expiry validation and revocation checks in the authorisation path. Starting a fresh local timer must never renew authority.

For the broader production workflow, see my Claude Code book, available as a Kindle edition on Amazon UK. This fixture is a separate teaching example, not an excerpt from the book.

Add a backwards-clock case and a retry-reset case to your adapter tests. Then test the real client’s cancellation path and record whether the remote effect was stopped, completed or left unresolved.