publish_and_wait can return the revision from the previous publish, and return straight away, before the security scan it exists to wait for has finished.
The module docstring explains the constraint it is working around:
publishing a draft returns only {state: "scanning"} (HTTP 202) while a security scan runs; there is no revision id in that response. The caller has to list the branch's newest revision and poll it until status == "published"
So wait_for_commit has nothing to match on, and identifies the new revision purely as "whatever is newest right now":
listed = self.revisions.list(id=agent_id, branch_id=branch_id, limit=1)
revs = getattr(listed.data, "revisions", None) or []
if revs:
rev_id = revs[0].id
...
if last_status == "published":
return rev
On a branch that has been published before, there is a window after publish_draft returns 202 scanning where the new revision row has not surfaced in the listing yet. During that window the newest revision is the previous one, which is already published, so the first poll matches on it and returns.
Reproducing
A stand-in client following the flow the docstring describes: publish_draft answers scanning with no revision id, and the listing returns the previous revision r1 on the first poll and the new r2 after that.
from types import SimpleNamespace as NS
from smallestai.atoms.helpers.versioning import Versioning
class FakeRevisions:
def __init__(self, script):
self.script, self.calls = script, 0
def list(self, id, branch_id, limit):
rev_id = self.script[min(self.calls, len(self.script) - 1)]
self.calls += 1
return NS(data=NS(revisions=[NS(id=rev_id)]))
def get(self, id, branch_id, revision_id):
return NS(data=NS(revision=NS(id=revision_id, status="published",
security_check=NS(status="passed"))))
class FakeBranches:
def publish_draft(self, id, branch_id, label=None):
return NS(data=NS(state="scanning", revision=None)) # 202, no revision id
class FakeClient:
def __init__(self, script):
self.atoms = NS(agent_versioning_branches=FakeBranches(),
agent_versioning_revisions=FakeRevisions(script))
v = Versioning(FakeClient(script=["r1", "r2"]))
got = v.publish_and_wait("agent-1", "branch-1", timeout=10.0, poll_interval=0.1)
print("publish_and_wait returned:", got.id)
publish_and_wait returned: r1
r2 is the revision this publish created. r1 is the one before it.
Why it matters
edit_and_publish hands that revision straight back, and the documented next step is branches.make_live(...). A caller that edits a prompt, gets what looks like a successful publish, and makes the branch live is acting on a revision that is not the edit they just made, while the real one is still being scanned. A scan that later fails is never seen, because the function already returned.
It is timing dependent, so it will look fine in testing and show up occasionally in production, which is the worst shape for this kind of bug.
A possible fix
Read the newest revision id once before calling publish_draft, then have wait_for_commit ignore that id and wait for a different one to appear and reach published. A branch with no revisions yet starts from None, so the first publish works unchanged.
That does mean wait_for_commit needs the baseline passed in when it is called from publish_and_wait, while staying usable on its own. Whether it takes an optional after_revision_id argument or publish_and_wait stops delegating and inlines the poll is your call, which is why I am raising it rather than sending a patch. Happy to send one once you have picked a direction.
Found while reading the helpers for #114, on main at fb2502c.
publish_and_waitcan return the revision from the previous publish, and return straight away, before the security scan it exists to wait for has finished.The module docstring explains the constraint it is working around:
So
wait_for_commithas nothing to match on, and identifies the new revision purely as "whatever is newest right now":On a branch that has been published before, there is a window after
publish_draftreturns202 scanningwhere the new revision row has not surfaced in the listing yet. During that window the newest revision is the previous one, which is alreadypublished, so the first poll matches on it and returns.Reproducing
A stand-in client following the flow the docstring describes:
publish_draftanswersscanningwith no revision id, and the listing returns the previous revisionr1on the first poll and the newr2after that.r2is the revision this publish created.r1is the one before it.Why it matters
edit_and_publishhands that revision straight back, and the documented next step isbranches.make_live(...). A caller that edits a prompt, gets what looks like a successful publish, and makes the branch live is acting on a revision that is not the edit they just made, while the real one is still being scanned. A scan that later fails is never seen, because the function already returned.It is timing dependent, so it will look fine in testing and show up occasionally in production, which is the worst shape for this kind of bug.
A possible fix
Read the newest revision id once before calling
publish_draft, then havewait_for_commitignore that id and wait for a different one to appear and reachpublished. A branch with no revisions yet starts fromNone, so the first publish works unchanged.That does mean
wait_for_commitneeds the baseline passed in when it is called frompublish_and_wait, while staying usable on its own. Whether it takes an optionalafter_revision_idargument orpublish_and_waitstops delegating and inlines the poll is your call, which is why I am raising it rather than sending a patch. Happy to send one once you have picked a direction.Found while reading the helpers for #114, on
mainat fb2502c.