The KVS docs say expiry is asynchronous: “Expired data is not removed immediately upon expiry. Deletion may take up to 48 hours. […] During this window, read operations may still return expired results.” The mitigation they give is read-side, request EXPIRE_TIME and ignore values whose expireTime has passed.
I can’t find the equivalent statement for a write. If a key is past its TTL but hasn’t been swept yet, does this throw?
await kvs.set(claimKey, { claimedAt: new Date().toISOString() },
{ keyPolicy: "FAIL_IF_EXISTS", ttl: { value: 10, unit: "MINUTES" } });
FAIL_IF_EXISTS plus a short TTL is the obvious way to build a lease. If a ghost key still counts as existing, a 10 minute lease can hold for up to 48 hours after it expires.
I know the custom entity route from 97917. The accepted answer there puts a version attribute on an entity and compares it inside transact().set(), which closes the takeover race without depending on TTL. The plain-key equivalent is what I can’t find. The KVS transactions page documents ttl and no conditions, so compare-and-swap seems to mean moving the claim into a custom entity and giving up keyPolicy entirely.
Inspecting the conflict isn’t an option either. I unpacked the published @forge/kvs tarball (2.0.3): returnMetadataFields appears on exactly one options variant, OverrideAndReturnSetOptions, which pins keyPolicy: 'OVERRIDE', and the overload that accepts keyPolicy at all returns Promise<void>. After the throw you have to re-read the key, which is a second round trip into the window you were trying to close.
That is what our own code does, in LeanZero’s Forge rule engine: the invocation claim function. We assumed the ghost does conflict, so on the error we re-read and compare claimedAt instead of trusting the TTL. I posted the flat version of this on 101889 in July, that a TTL isn’t a dedup window because deletion is lazy. That was about reads. I don’t know whether it holds for a conditional write, and I haven’t run it: a sweep that happens to fire on a dev site isn’t a contract, and this is the kind of thing that could change without a note.
Is an expired but unswept key visible to FAIL_IF_EXISTS?