AI agents

A Promotion Queue: Routing Draft Writes to a Second Source Check

Antonio Evans8 min read

A wide horizontal diagram on a white background.
Key points

Key points

  1. 1The promotion queue is a separate service that takes a draft write, reads a second independent source with its own credentials, and records an explicit promote, drop or review decision for every row.
  2. 2The checker must live outside the writer, because a second read from the same connection returns the same cached, scoped or permission-filtered answer and confirms nothing.
  3. 3The staleness cutoff is evaluated against check time, not draft time, so a draft can still be dropped when its verification arrives too late to be trusted.
  4. 4Each row keeps the draft id, both values, check time, staleness verdict, decision, actor and a reason code, which is what turns the rule into an auditable trail.

Why repetition is not verification

A draft write is a claim the agent proposes to keep: a user preference, a fact about a service, a procedure that seemed to work. The tempting promotion rule is repetition. If the claim shows up twice, promote it. GovMem's authors studied exactly this failure and named it false promotion: repeated observations across agents are not necessarily independent evidence, because the same claim may be copied from a shared source, induced by a shared prompt, stale under a new environment, or valid only in a narrower scope (the GovMem paper). In controlled stress tests the conservative policy cut false promotion from 0.597 to 0.040 while holding recall at 0.960, at the cost of an explicit review burden (GovMem). That trade is the whole argument for a queue: you are buying a lower false-promotion rate with reviewer time, and you should know the exchange rate before you build it.

The same asymmetry shows up in memory poisoning work. Zep's defense write-up notes that ten messages from one account remain one source, and that a model's restatement of a claim is not a new independent source (Zep on memory poisoning). A summary of end-user content is still end-user content. If your second check is another pass by the same model over the same retrieval results, you have built a corroboration theater, not a verification step.

So the first design commitment is structural, not procedural. The checker is a different process, with different credentials, reading a different system of record.

The checker is a separate service with its own credentials

Putting the check inside the writer is the most common shortcut and it fails for three reasons. The writer's credentials usually narrow the answer: a service account scoped to one tenant, one workspace or one environment reads a cached or filtered view. The writer's process shares state with itself: connection pools, request-scoped caches and in-process memoization return the value it just wrote. And the writer's rate limit is the writer's, so a burst of drafts cannot be checked at a different pace than they were produced.

The checker should therefore hold a read-only credential on the second source, have its own rate limit budget, and expose one narrow interface: given a claim (subject, field, asserted value, scope), return the observed value, a read time, and a source identifier. Nothing else. A generic "verify this" endpoint invites the checker to become a second writer.

This is the same principle the memory literature calls controlled writes: one application-owned path decides what enters durable memory, and the model proposes while application code decides (Zep). It also matches the pattern atlas's insistence that evidence be preserved before belief, so that correction is possible later (Agent Memory Atlas). A queue row is evidence. The promoted value is belief.

There is a practical side benefit. Once the checker is a service, you can rate-limit it per source and per time window, which is precisely the countermeasure against flooding and fake corroboration that Zep describes. A claim that arrives fifty times in a minute hits the same per-source budget whether it came from one account or fifty.

The staleness cutoff, evaluated at check time

The second source can be correct and still useless. Its read has an age, and that age matters more than the draft's age, because the read is what you are about to trust. This is the distinction the queue enforces: the cutoff is a maximum age for the second source read, measured from check time backward to the read time, not from draft time forward.

A worked example makes the arithmetic concrete. Suppose the cutoff is 24 hours.

Draft writtenSecond source readCheck runsSource age at checkVerdict
Mon 09:00Mon 09:05Mon 09:061 minuteFresh, compare values
Mon 09:00Mon 09:05Tue 10:3025 h 25 mStale, re-read or drop
Mon 09:00unknownThu 12:00unknownStale, no read on record
Mon 09:00Tue 23:00Tue 23:044 minutesFresh, compare values

Row two is the case that separates the two clocks. The draft is more than a day old, but so is the read, and the read is the thing that has gone off. If you had measured from draft time, row four would have been dropped for a draft that was 38 hours old even though its verification was four minutes old.

The cutoff number itself should come from how fast the second source changes, not from taste. If the second system of record is updated by a batch job every few hours, a 24 hour cutoff tolerates several missed batches and is probably too loose. If it is updated continuously, 24 hours is generous. Set the cutoff so that a read at the boundary is still likely to match what a fresh read would return, and write that reasoning into the queue's configuration so the next person does not have to guess.

Forgetting research makes the same point from the other direction: staleness is a recall-quality problem, not a storage problem, and age-based expiry is the crudest of the four eviction strategies, right for policy-bound data and wrong for facts that stay true (mem0 on eviction and forgetting). A staleness cutoff is an age policy applied to a read, and it should be paired with a reason code so you can see how often it fires.

Note also what a TTL on the second source does and does not guarantee. The MongoDB agent-memory documentation is unusually honest here: deletion is not immediate, and a retention change that silently fails leaves data expiring on the old schedule (configure TTL). If your checker reads from a store with its own retention policy, a missing value is ambiguous. Record "no read" as a distinct verdict, not as a mismatch.

The row: fields, reason codes, and what each one buys you

Every draft that enters the queue produces exactly one row, and the row is the audit record. The minimum field set:

FieldTypeWhy it is there
draft_idstring, immutableJoins the queue row to the draft and to every later derivative
scopeuser, project, tenantRetrieval and review are scoped before ranking is scored (Agent Memory Atlas)
source_a_valuetypedWhat the draft asserted
source_b_valuetyped or nullWhat the independent checker observed
source_b_idstringWhich second source was read
read_timetimestampWhen the checker read it, in UTC
check_timetimestampWhen the queue evaluated the row
staleness_verdictenumfresh, stale_replaced, stale_no_read, stale_unknown
decisionenumpromote, drop, needs_review
reason_codeenumWhy, from a closed list
actorstringWho or what decided: checker, rule id, or a human id
decided_attimestampWhen the decision was recorded

Two of these deserve emphasis. Reason codes must be a closed enumeration, not free text, or you cannot aggregate them and the queue becomes a log rather than a control. And actor should be able to name a rule, not just a person, so that an automatic promotion by rule.independent_match.v3 is distinguishable from a reviewer's override. Microsoft's audit activity tables show the same shape at platform scale: an agent tool call, an inference call and a guardrail application are separate, named operations (Microsoft Purview audit activities).

A compact reason code set that has held up in practice:

PROMOTE_INDEPENDENT_MATCH      both values equal, source fresh, different credentials
PROMOTE_SCOPED_MATCH           equal within the draft's declared scope only
DROP_CONTRADICTED              source B differs and is fresh
DROP_STALE                     source B read older than cutoff
DROP_NO_READ                   checker returned no value
DROP_SCOPE_MISMATCH            source B answered for a different scope
NEEDS_REVIEW_AMBIGUOUS         values comparable but not equal (format, units, rounding)
NEEDS_REVIEW_HIGH_IMPACT       rule flags this field for human sign-off
NEEDS_REVIEW_CHECKER_ERROR     checker failed; not evidence either way

The decision enum collapses to promote, drop or needs_review, which is the same three-way policy GovMem uses. Keeping needs_review as a first-class outcome is what makes the review burden measurable rather than invisible; GovMem reports it explicitly, at 0.692 in its human-labeled subset (GovMem).

Read time and decision time are separate columns for the same reason event time and acceptance time are stored apart in defensive memory design: a later date does not make a claim more trustworthy, and collapsing the two clocks hands an attacker the lever (Zep).

Operating the queue

Intake. The writer does not decide. It enqueues a draft with its scope, its asserted value and a typed subject. The queue is append-only; a resubmission of the same claim is a new row that references the original, which gives you the dependency information you need to spot fifty copies of one source.

Check. The checker reads source B, records the value and the read time, and returns. It does not compare, decide or write anything durable. This keeps the checker cheap to test and impossible to blame for a bad promotion.

Evaluate. The queue applies rules in a fixed order: scope mismatch, then staleness, then value comparison, then impact flags for needs_review. Fixed order matters because a stale read that contradicts must never be reported as a contradiction.

Record and act. The row is written before the promoted value is written to long-term memory, so a crash leaves an orphan row you can replay rather than a silent promotion with no record. Replay is idempotent on draft_id.

Review. Needs_review rows go to a human with the row, not with a paragraph of model prose. The reviewer's actor id lands in the same table.

The compliance case for the extra ceremony is straightforward. SOC 2's Security criterion expects logging and monitoring as primary evidence, with audit logs retained beyond the lifetime of the systems that produce them and stored in tamper-evident archives, and Type II reports cover an observation period of typically six to twelve months (SOC 2 logging pipelines). A table that answers who, what, when, why and what the value was before is exactly the artifact that question requires (audit logs in data platforms).

Before you build, write the capacity numbers down. If the writer produces 10,000 drafts a day and the checker can read 4,000 claims an hour, the queue will fall behind by design unless you batch by subject and field. Batching is fine as long as read_time reflects the batched read, not the batch start.

If your agent fleet already spans several model providers and you are wiring the checker to compare model-authored claims against a source of record, Quanta Inference gives you one OpenAI-compatible key and US-dollar prepaid pricing across text, image, video, audio and embedding models, which keeps the checker's model dependency out of its credential story.

Where this breaks

Three failure modes are worth naming because each has a detectable signature.

The checker that echoes. If source B is populated by the same pipeline that produced the draft, promotion rates will look excellent and mean nothing. Detection: measure the rate of DROP_CONTRADICTED. A healthy independent source contradicts sometimes. A zero rate on a large sample is a warning.

The staleness verdict that never fires. If DROP_STALE is never emitted, either your checker is fast or your cutoff is not being evaluated against read_time. Log source age as a histogram on every row and check the tail.

The review queue that becomes a rubber stamp. Needs_review rows resolved in under a few seconds, consistently, by one actor, indicate that the human step is decorative. GovMem's external coding-agent adjudication is a sobering reference point: none of 133 high-impact candidates were safe for automatic promotion (GovMem). If your approval times look like that dataset, assume the same.

One more: recorded decisions age. A promoted value with a fresh check six months ago is not evidence of anything today. Give promoted values their own age column and re-queue them at a slower cadence, so the queue is also the mechanism for catching drift rather than only for catch-and-release at write time.

FAQ

FAQ

Why can't the checker be a function inside the writer?

Because a function inside the writer inherits the writer's credentials, caches and request scope, so it tends to re-read the value the writer just placed rather than an independent system of record. Its answers will agree far more often than the truth does, and you will read that agreement as evidence.

What exactly does the staleness cutoff measure?

The age of the second source read, from its read time to the moment the queue evaluates the row. It is not the age of the draft. A draft can be young and its verification old, and when the two disagree the verification's age is the one that decides whether you may trust it.

Which fields must a queue row contain?

At minimum: draft id, scope, both values, the second source identifier, read time, check time, staleness verdict, decision, reason code, actor and decision time. Read time and decision time stay separate columns so that a late acceptance cannot masquerade as a fresh read.

How many decisions should the queue produce?

Three, not two. Promote, drop and needs_review. Collapsing review into drop hides the human effort you are spending, and collapsing it into promote turns an uncertain claim into a durable one with nobody's name on it.

How do I know the second source is actually independent?

Check the credentials, the process boundary and the provenance of the store. If source B is written by the same pipeline, drawn from the same account, or restated by the same model, it is not a second source, no matter how different the value looks. Measure the contradiction rate; a source that never disagrees is not checking anything.

Sources

Sources

  1. When Not to Write Memory: Governing False Promotion ...
  2. Defending Agent Memory Against Poisoning | Zep Blog
  3. Agent Memory Atlas
  4. Memory eviction and forgetting in AI agents
  5. agent-memory/docs/how-to/configure-ttl.md at main
  6. Audit log activities
  7. SOC 2 Logging Pipelines: Key Requirements, Steps, and ...
  8. Audit Logs Are Not Optional: The Hidden Backbone of ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles