Auten gives your AI agent hands: it clicks, types and reads a real screen on your computer (Mac/Windows/Linux) and Android phone (beta). Works with any MCP client.
The "we don't know read as yes" scene is the one worth underlining. On the GUI side, where an agent's "send" is a real click in a real app, there is no idempotency key at all, so a released wait after a failed write means the message really goes out twice. Failing closed is the only safe default there.
Two things that might fit your model:
Bind the approval to the exact arguments the person saw (a hash of the payload on the row, checked in the same conditional update). If the model retries with slightly different arguments, the update misses, because nobody approved that version.
A row decided late from the pending list lands after the conversation has moved on. Telling the agent explicitly "approved later, executed at T" keeps it from redoing the step on its own.
Does a late approval in KeelBase execute right away, or wait until the conversation picks it back up?
Right away. The conditional update is the decision, and the execution happens in the same request, so a row approved from the pending list runs at that moment rather than when the conversation resumes.
The conversation is deliberately not woken. Unblocking it would let the original stream execute the same tool a second time, because that branch only knows approve. So the stream waits out its own window and reports a timeout, and the cost is recorded in the code: the tool ran, and the agent was told it did not. Your second bullet names what that costs, and we do not have the notification it asks for.
How much it costs depends on the tool. A retry of the same call in the same conversation hits the idempotency key and gets the recorded result back. A tool outside that path, an external one with no key or anything that clicks a real interface, has nothing to hit, so the retry is a second execution. That is your first paragraph arriving through a different door.
On binding to what the person saw: the row already carries the exact arguments, and both execution paths run the arguments from the row rather than from wherever the retry came from, so a retry with different arguments is a different token and never reaches this one. A hash inside the conditional update would be defence in depth against a later change to that invariant. I would take it for that reason rather than because the hole is there today.
On the GUI surface I can only agree from the outside. We do not cover it, which is why your paragraph is the right argument to make there.
Auten gives your AI agent hands: it clicks, types and reads a real screen on your computer (Mac/Windows/Linux) and Android phone (beta). Works with any MCP client.
Thanks, that answers it, and the reason not to wake the stream makes sense: approve-only branch plus a live stream is exactly how you get the second execution.
One cheap way to shrink that cost without waking anything: when the wait times out, return "pending, not confirmed" instead of a plain timeout, plus the row id. An agent that reads "not run" retries; one that reads "pending, check row X" usually asks first. For the keyless tools that is the difference between one send and two.
Agreed on the hash: an invariant guard, not a fix for a hole.
That one is cheap enough to take, and it is cheaper than it looks from outside. A timeout here already returns an explanation alongside the error, with both windows in it, so the shape exists. What is wrong is the sentence at the top, which reads as "not run", and what is missing is the handle.
The handle is already in scope where the message is built, so this is passing a value through rather than plumbing a new one. One wrinkle you cannot see from the post: at run level the identifier is the run, not a confirmation row, so the sentence should not pretend the two are the same object.
Whether it actually stops the retry, I do not know and would not claim. "Pending, check row X" making an agent ask is a plausible mechanism, and the only way I would learn it here is to count it: the same tool in the same conversation, called again within some minutes of a timeout, measured before and after the change.
Auten gives your AI agent hands: it clicks, types and reads a real screen on your computer (Mac/Windows/Linux) and Android phone (beta). Works with any MCP client.
Agreed on keeping the run and the row as separate things. Saying "run R timed out, its confirmation is still pending" is honest, and it still gives the agent something to quote back when it asks.
For the count, I'd match on the same tool plus the same arguments hash within a few minutes of a timeout. A retry with different arguments is usually the agent changing its plan, not a double send, so leaving those out keeps the before/after number clean.
That phrasing is better than mine and I will use it.
On the count, your key is right and we happen to already compute it: the key that decides idempotency here hashes the user, the conversation, the tool and the sorted arguments, so matching on tool plus argument hash is the same shape. My first version of the metric was missing that dimension.
What I would change is where the counter sits. After a timeout nothing ran, so there is no recorded result to replay, and a retry looks like the first execution of that call rather than a replay of it. So the count belongs at confirmation creation: the same key asking for a second confirmation inside the window. That is the number that will move if the wording changes the behaviour.
I'd include the offline deadline in the same conditional update that checks whether the row is pending. Otherwise an approval arriving after 24 hours could still win before the maintenance sweep changes the status, even with both paths computing the same boundary. A test that pauses the sweep, advances past the deadline, and races the two approval entries would expose that gap. I'd also test the exact deadline instant so the list, decision entry and sweep agree on whether equality is still actionable.
You are right, and the window is wider than a race. The sweep runs hourly, so an approval landing anywhere inside that hour still wins. The conditional update checks the token, the owner and pending, and nothing about the deadline. The deadline is enforced only afterwards, by the sweep that flips the row.
Which sharpens your second point. Wiring the deadline into the condition has to use the same inclusive edge the list uses, or the fix creates the disagreement it was meant to remove: at exactly the cutoff, the list offers the row and the decision would refuse it.
Both tests are the ones I would want, and the first is the only way to see this at all. With the sweep running, the gap is under an hour and nobody notices. With it paused, two entries race on a row that should already be dead.
One thing that does not need fixing, for what it is worth: the in-conversation path is already safe, because its timer drops the pending entry and a late click returns false there. The hole is only on the out-of-band path.
The distinction between “still waiting” and “still actionable” is easy to miss until it causes a real bug. I like that the solution separates the two timeouts instead of trying to make one value handle both responsibilities.
The split came out of a bug, not a design session. The list kept offering a row as still actionable after the sweep had already killed it, so the click was a guaranteed failure. Two values were the only way to make the list and the sweep agree on the same instant.
The part I would not call finished is the case where the decision arrives late and the conversation never hears it. That one needs a message, not a value.
We're a place where coders share, stay up-to-date and grow their careers.
The "we don't know read as yes" scene is the one worth underlining. On the GUI side, where an agent's "send" is a real click in a real app, there is no idempotency key at all, so a released wait after a failed write means the message really goes out twice. Failing closed is the only safe default there.
Two things that might fit your model:
Does a late approval in KeelBase execute right away, or wait until the conversation picks it back up?
Right away. The conditional update is the decision, and the execution happens in the same request, so a row approved from the pending list runs at that moment rather than when the conversation resumes.
The conversation is deliberately not woken. Unblocking it would let the original stream execute the same tool a second time, because that branch only knows approve. So the stream waits out its own window and reports a timeout, and the cost is recorded in the code: the tool ran, and the agent was told it did not. Your second bullet names what that costs, and we do not have the notification it asks for.
How much it costs depends on the tool. A retry of the same call in the same conversation hits the idempotency key and gets the recorded result back. A tool outside that path, an external one with no key or anything that clicks a real interface, has nothing to hit, so the retry is a second execution. That is your first paragraph arriving through a different door.
On binding to what the person saw: the row already carries the exact arguments, and both execution paths run the arguments from the row rather than from wherever the retry came from, so a retry with different arguments is a different token and never reaches this one. A hash inside the conditional update would be defence in depth against a later change to that invariant. I would take it for that reason rather than because the hole is there today.
On the GUI surface I can only agree from the outside. We do not cover it, which is why your paragraph is the right argument to make there.
Thanks, that answers it, and the reason not to wake the stream makes sense: approve-only branch plus a live stream is exactly how you get the second execution.
One cheap way to shrink that cost without waking anything: when the wait times out, return "pending, not confirmed" instead of a plain timeout, plus the row id. An agent that reads "not run" retries; one that reads "pending, check row X" usually asks first. For the keyless tools that is the difference between one send and two.
Agreed on the hash: an invariant guard, not a fix for a hole.
That one is cheap enough to take, and it is cheaper than it looks from outside. A timeout here already returns an explanation alongside the error, with both windows in it, so the shape exists. What is wrong is the sentence at the top, which reads as "not run", and what is missing is the handle.
The handle is already in scope where the message is built, so this is passing a value through rather than plumbing a new one. One wrinkle you cannot see from the post: at run level the identifier is the run, not a confirmation row, so the sentence should not pretend the two are the same object.
Whether it actually stops the retry, I do not know and would not claim. "Pending, check row X" making an agent ask is a plausible mechanism, and the only way I would learn it here is to count it: the same tool in the same conversation, called again within some minutes of a timeout, measured before and after the change.
Agreed on keeping the run and the row as separate things. Saying "run R timed out, its confirmation is still pending" is honest, and it still gives the agent something to quote back when it asks.
For the count, I'd match on the same tool plus the same arguments hash within a few minutes of a timeout. A retry with different arguments is usually the agent changing its plan, not a double send, so leaving those out keeps the before/after number clean.
That phrasing is better than mine and I will use it.
On the count, your key is right and we happen to already compute it: the key that decides idempotency here hashes the user, the conversation, the tool and the sorted arguments, so matching on tool plus argument hash is the same shape. My first version of the metric was missing that dimension.
What I would change is where the counter sits. After a timeout nothing ran, so there is no recorded result to replay, and a retry looks like the first execution of that call rather than a replay of it. So the count belongs at confirmation creation: the same key asking for a second confirmation inside the window. That is the number that will move if the wording changes the behaviour.
I'd include the offline deadline in the same conditional update that checks whether the row is pending. Otherwise an approval arriving after 24 hours could still win before the maintenance sweep changes the status, even with both paths computing the same boundary. A test that pauses the sweep, advances past the deadline, and races the two approval entries would expose that gap. I'd also test the exact deadline instant so the list, decision entry and sweep agree on whether equality is still actionable.
You are right, and the window is wider than a race. The sweep runs hourly, so an approval landing anywhere inside that hour still wins. The conditional update checks the token, the owner and pending, and nothing about the deadline. The deadline is enforced only afterwards, by the sweep that flips the row.
Which sharpens your second point. Wiring the deadline into the condition has to use the same inclusive edge the list uses, or the fix creates the disagreement it was meant to remove: at exactly the cutoff, the list offers the row and the decision would refuse it.
Both tests are the ones I would want, and the first is the only way to see this at all. With the sweep running, the gap is under an hour and nobody notices. With it paused, two entries race on a row that should already be dead.
One thing that does not need fixing, for what it is worth: the in-conversation path is already safe, because its timer drops the pending entry and a late click returns false there. The hole is only on the out-of-band path.
The distinction between “still waiting” and “still actionable” is easy to miss until it causes a real bug. I like that the solution separates the two timeouts instead of trying to make one value handle both responsibilities.
The split came out of a bug, not a design session. The list kept offering a row as still actionable after the sweep had already killed it, so the click was a guaranteed failure. Two values were the only way to make the list and the sweep agree on the same instant.
The part I would not call finished is the case where the decision arrives late and the conversation never hears it. That one needs a message, not a value.