Case Study · Building with AI
When a tool times out: outbox lessons for reliable AI actions
A source-level case study of MaybeTomorrow’s queued intent, cancellation, and stale acknowledgements, with a worksheet for designing reliable AI tool actions.
In this article
A timeout does not tell you whether an action happened
An assistant asks a tool to create a task. The network times out. Should it try again? The answer depends on something the timeout does not reveal: the server may have created the task and lost the response, or it may never have received the request. Repeating an action without accounting for that uncertainty can create duplicates.
The operator's MaybeTomorrow planner offers a useful case study in the ordinary software needed around such actions. Its local outbox records pending intent, associates it with an account and entity, coalesces edits, distinguishes deletion scenarios, and guards completion against a newer local change. These mechanisms address synchronization, but the reasoning transfers directly to AI tools that act on persistent state.
For this article, we inspected the planner's Swift outbox implementation and its focused test source. We did not run the iOS application, simulate a production outage, or test an AI agent against the planner. This is an implementation case study with an explicit boundary: the observed evidence is local code, and the agent examples are proposed applications of the same state-management ideas.
Original TrendsWhat diagram redrawn from the inspected outbox code. A response to an older version must not erase a newer pending change.
Start with intent that survives the request
An outbox is a local table of work that still needs synchronization. In MaybeTomorrow, each entry includes an account key, entity type, local entity identifier, mutation type, payload, status, attempt count, error, and timestamps. The table has a unique index across account, entity type, and local identifier.
That design gives a pending action an identity independent of a particular network request. If a request fails, the intent can remain available for another attempt. If the person edits the same entity again, the application can update the pending intent instead of creating an unrelated chain of actions that must all be replayed.
For an AI tool, this distinction separates planning from transport. “Create this task” is a user-level intention. An HTTP attempt is one way to deliver it. A retry should continue the same authorized intention, not silently turn it into a second instruction merely because the assistant saw an error message.
What the local code actually shows
The inspected OutboxRepository creates an OUTBOX_MUTATIONS table and account-scoped indexes. An upsert operation updates an existing row for the same entity or inserts a new one. Updating resets retry bookkeeping, allowing a fresh local edit to be treated as current work. The implementation preserves the latest upsert intent even when the previous queued mutation was a deletion.
Deletion receives additional context: whether the entity has a server identifier and whether creation is in flight. It returns one of three decisions. A purely local entity with no queued server work can be removed locally. A pending creation that never left the device can be canceled. An entity that exists, or may be being created, needs a deletion intent to reach the server.
The completion method deletes an outbox row only when both its ID and expected update timestamp match. If a newer local edit changed the timestamp, an acknowledgement for the older version does not remove the updated row. That condition is small in code but central to the behavior described in this case study.
Why deletion needs more than a boolean
Consider three illustrative situations. In the first, a person creates a task offline and deletes it before any request starts. Sending both creation and deletion later is unnecessary if the local system can safely cancel the pending create. In the second, the create request has started but has no response. The server may still create the task, so deleting only the local record risks leaving a remote object behind.
In the third, the task already has a server identifier. The remote deletion must be synchronized. The inspected code distinguishes these states rather than treating “no server ID yet” as proof that the server has never seen the object. That is an important difference because in-flight work creates uncertainty.
An AI assistant faces a comparable issue when canceling an action after a tool call starts. “I canceled it” should mean something precise. Was the request never sent, was a pending local operation canceled, or was a compensating delete queued? A trustworthy interface reports the state it knows rather than converting uncertainty into a confident completion message.
The version guard in slow motion
Imagine an outbox row for task A at version time T1. The application sends that version to the server. Before the response arrives, the person edits task A again, and the local row becomes T2. The server responds to the earlier request. An unconditional delete by row ID would remove the T2 work even though the server acknowledged only T1.
The inspected removal method includes the expected timestamp in its condition. A response associated with T1 cannot delete the row now marked T2. The newer intention survives for a later synchronization pass. This is an optimistic concurrency guard: the operation proceeds only if the stored state still matches the version it expects.
The concept transfers beyond timestamps. A monotonic version number or another explicit revision token can play the same role. What matters is that completion refers to the version actually sent. The article does not claim that timestamp precision resolves every possible race in the planner; verifying that would require a broader concurrency test.
Read the implementation as well as its comments
Source inspection revealed a useful teaching point: comments describe intent, while executable statements determine behavior. The upsert comment mentions a delete-winning rule, but the body updates an existing row to an upsert. A later inline comment explains that the latest user edit is authoritative. For this case study, we describe the body rather than repeating the outdated summary.
The retry helper offers another example. It computes two raised to the attempt count capped at eight, then applies a three-hundred-second minimum ceiling operation. Because two to the eighth is 256, the inspected function's computed delay stops growing at 256 seconds. Calling it a five-minute delay would be less precise than describing the expression that actually runs.
These observations are not claims of a production incident. They show why AI-assisted code reading should compare comments, tests, and implementation instead of summarizing the nearest explanation. When two sources disagree, preserve the disagreement and inspect the relevant branch before drawing a conclusion.
What the test source adds, and what it does not
The local test file defines in-memory SQLite cases for repeated upserts, deletion without server state, cancellation of an unsent create, deletion during an in-flight create, ordering, and the expected-timestamp removal guard. This is useful evidence of intended edge cases and gives a maintainer a focused place to add a regression example.
We inspected those test definitions; we did not execute the iOS test suite for this article. Their presence is therefore not a claim that the current checkout passes them. A test can be outdated, incomplete, or disconnected from a runtime configuration. The distinction matters whenever a case study uses a repository as evidence.
For an AI coding assistant, an appropriate summary would say, “There is a test for stale acknowledgement removal,” followed by whether it was run and its result. Saying “This is verified” merely because the test file exists skips the part that establishes current behavior. Good evidence language should make that gap visible.
A reusable action-state worksheet
| State or event | Question your workflow must answer |
|---|---|
| Draft intention | What exactly did the user authorize? |
| Queued action | Where is the durable action identifier? |
| In flight | Can the server have applied it already? |
| Timeout | What state is unknown, and how will it be checked? |
| Newer local edit | Which revision should the acknowledgement affect? |
| Retry | Is this the same action or a new action? |
| Cancellation | Was work prevented, canceled locally, or compensated remotely? |
| Completion | Which stored state proves the result? |
Use this table for any AI tool that creates, updates, or removes persistent records. It is especially useful when an assistant can issue several tool calls in succession. Without a durable action identity, a natural-language retry instruction can accidentally widen the scope of the original request.
The table does not mean every workflow needs a full mobile outbox implementation. A simple server operation may use an idempotency key and a status endpoint instead. The appropriate mechanism depends on the system. The transferable requirement is to represent uncertainty and revisions explicitly enough that a retry does not rely on guesswork.
How to validate a comparable system
Begin in a test environment with fictional records. Create an operation, interrupt its response path, and inspect the authoritative state before retrying. Confirm that repeated delivery does not create a second logical result. Then edit the record while an earlier request is in flight and check that the old acknowledgement does not erase the new work.
Test cancellation at different moments: before send, during send, and after acknowledgement. Verify account separation by using distinct test accounts and checking that pending work is drained only within the correct account scope. Finally, record the exact retry delays from the implementation rather than copying them from a comment.
These are proposed validation steps for readers. They were not executed against the operator's production planner. The article's original contribution is the source-level analysis and the reusable state worksheet, while the earlier TrendsWhat labs provide examples of separately executed deterministic experiments.
The AI skill hiding inside synchronization code
It is tempting to judge an agent by whether it selects the right tool. A more demanding question is whether it can explain the state after the tool returns ambiguously. A timeout, a stale acknowledgement, and a canceled local draft are different situations. Treating all three as a generic failure erases information needed for the next safe action.
MaybeTomorrow's outbox makes those distinctions concrete. It records intent, keeps account and entity identity, and conditions completion on the version that was sent. The same reasoning helps a person supervise AI: ask what happened, what is known, and what evidence would justify the next attempt.
Reliable action is not only about choosing a command. It is about preserving the meaning of the user's request while requests fail, responses arrive late, and the underlying state changes. That is a practical engineering lesson worth carrying into any AI workflow with consequences beyond a draft paragraph.
Sources, materials, and limits
- Source inspection notes: MaybeTomorrow outbox and test file locations, reviewed September 15, 2026.
- Operator portfolio: public attribution for the planner project, used as context rather than runtime proof.
- Original TrendsWhat architecture diagram and action-state worksheet.
- Local source inspection only; test definitions read but not run; no production writes, incident reconstruction, mobile runtime validation, or AI-agent benchmark.