Ask a capable model to review a skincare ad with an icy blue background and it will tell you to push the blue further: "a deeper, more saturated blue or teal accent to reinforce a clinical feel." That's reasonable advice. It's also exactly what this client has rejected twice. Their founder's words: "No. I said this back in February: blue feels like a pharmacy. We are warm neutrals."
That gap is the whole reason I built Round Zero. It's a pre-reviewer for creative agencies: before a design draft goes to a client, it predicts the feedback the client would give, and every issue it raises quotes the client's own words, with who said it, on which project, and when.
The model was never the hard part. Memory was, and specifically getting memory to understand time. This post is about that.
What it does
You pick a client and upload a draft. Round Zero:
- Measures the draft. Colours and texture come from the pixels; layout, type and copy from a vision model (Qwen 3.8 27B on Groq).
- Recalls what the client has said about each aspect.
- Proposes issues, each citing specific client feedback.
- Verifies every issue and resolves the citations to real quotes.
When the client responds for real, their feedback goes into the same memory.
The memory layer is Hindsight, an open-source agent memory engine. Each client gets a memory bank with a mission and directives, which are hard rules like "no drug claims" for a skincare brand. retain() stores feedback, Hindsight consolidates it into observations (the client's taste profile), recall() finds what's relevant to a draft, and reflect() reasons over it with the directives applied.
draft.png ─► palette.py (pixels) ┐
└► vision.py (Groq) ├─► recall ×4 ─► reflect ─► verify ─► flags + quotes
client bank (Hindsight) ─────────────┘ ▲
client feedback ──── retain() ────────────────┘ (observations consolidate in the background)
To test it, I wrote three client histories modelled on typical agency feedback threads: a conservative law firm, a Gen-Z snack brand and a direct-to-consumer skincare brand. That's 27 projects and 91 dated client quotes, including stakeholders who contradict each other and one full rebrand.
The rebrand problem
The snack brand, Crunchd, spent 2025 as a soft pastel brand. Its head of brand's first note on the first draft: "this is giving 2008 supermarket flyer. we're soft, not loud." Later, on the neon glitter version: "no neon. ever. it cheapens us."
Then, in June 2026, they rebranded: electric orange, acid lime and hot pink on black, with chunky condensed type. The same person, in the rebrand brief: "yes, this means neon is ok now lol. i know i said never."
A reviewer that remembers everything but understands nothing about time is worse than one with no memory. It would flag the new brand's drafts for using neon. I wanted one pastel draft to come back ready for client against the April 2026 memory and flagged against the September one.
Retain in order, with real timestamps
Memory has to be written the way it happened. The seed script retains every quote in chronological order with its original timestamp, and after each project it polls Hindsight's Operations API until consolidation finishes. Each quote is its own document, with author, role, project and date as metadata.
Then the rebrand didn't land
After the first full seed, I looked at the after-rebrand bank's observations. The new palette was in there. So was "Neon colors are prohibited in all brand designs", untouched. So was "brand identity is defined as soft rather than loud". Hindsight hadn't updated the old beliefs. It had added new ones next to them.
The cause was my own tagging. I tag every memory with its client, project and phase so I can filter recall later. Hindsight's default observation scope is combined: a memory only consolidates with memories that carry exactly the same tag set. By tagging per project, I had split each client into one isolated taste profile per project. The rebrand feedback lived in the project:cr-08 island and never met the neon rule from project:cr-05.
The Hindsight docs on retain cover the alternatives: per_tag, shared, all_combinations, or an explicit list. I set the scope explicitly, per memory:
@property
def observation_scopes(self) -> list[list[str]]:
# Hindsight's default ("combined") consolidates only among memories with the exact
# same tag set, which would silo every project. All of a client's feedback must feed
# one taste profile, so observations are scoped to the client tag alone. Project and
# phase tags stay on the facts for recall filtering.
return [[f"client:{self.client_id}"]]
retain() doesn't take that parameter in the Python SDK, but retain_batch() does, so every write goes through one helper:
def retain_item(bank_id: str, item: MemoryItem) -> None:
# retain_batch rather than retain(): only the batch form accepts observation_scopes.
hindsight().retain_batch(
bank_id=bank_id,
items=[{
"content": item.content,
"timestamp": item.timestamp,
"context": item.context,
"document_id": item.document_id,
"metadata": item.metadata,
"tags": item.tags,
"observation_scopes": item.observation_scopes,
}],
)
Proof counts jumped straight away. Once it could draw on every project, the law firm's palette observation was backed by 17 pieces of feedback.
Then it over-corrected
With everything in one scope, the after-rebrand bank collapsed to 5 observations. My observations mission had said "prefer one observation per rule", so Hindsight built a catch-all "soft aesthetic" observation holding lowercase type, grain texture and a ban on "ALERT". When the rebrand contradicted part of it, consolidation replaced the whole thing, and rules the rebrand never touched disappeared with it.
The fix was in the mission text: exactly one topic per observation ([Colour], [Typography], [Copy]…), and update a reversed rule in place rather than delete it. After re-seeding, untouched rules survived, and the [Compliance] observation's history now shows the founder's April worry about "becoming too generic" turning into "adopted a LOUD identity". The colour observation was still replaced rather than updated, so the history doesn't link back to the pastel version. I stopped tuning there: the demo compares snapshot banks from before and after the rebrand, which Hindsight's clone_bank makes cheap.
Making reflect show its receipts
For the review itself, I recall once per draft aspect (colour, typography, layout and imagery, copy) and number every returned quote in date order: F1 through F36, oldest first. Then I call reflect() with a response schema that requires evidence IDs on every flag:
resp = hindsight().reflect(
bank_id=bank_id,
query=query, # the draft description and the review rules
context=context, # numbered, dated client quotes: "[F12] 2025-10-09 · Amara Nwosu (Founder) · ..."
budget="mid",
response_schema=REVIEW_JSON_SCHEMA,
include_facts=True,
apply_all_directives=True,
)
Because each quote was retained as its own document, resolving F12 back to "Amara Nwosu, Founder, winter barrier kit banner, 9 Oct 2025" is a dictionary lookup, not a guess.
I didn't expect to need a second model after reflect. But in one run, reflect produced four correct flags with empty evidence arrays; my resolver dropped them, and a draft that should have been rejected came back "ready for client". It also sometimes applied a photo-grading rule to a vector illustration. Now a verifier (Groq's gpt-oss-120b, temperature 0) checks each flag: is the problem really in the draft, is the instruction still current, does it apply to this kind of asset? It also fills in missing evidence IDs.
What it does now
On the rebrand draft:
- Before-rebrand memory: ready for client. It matches the pattern the client approved in March 2025.
- After-rebrand memory: three high-severity flags (palette, grain, type), each citing feedback from after the rebrand, such as the founder's "Drop the grain and the vintage grade. Crisp, punchy, high contrast. The faded look is the old brand." (18 June 2026)
- Without memory: the same "make it bolder" advice at both dates, so it's wrong at one of them.
On the skincare draft, with eight problems planted in it:
- Without memory: 2 of 8, plus the advice to make it more clinical.
- With memory: 6, 8, 6 and 7 of 8 across four runs of the final version.
- Every run caught the drug claim, the before/after split, the front-on bottle, the missing ingredient percentages and the gradients.
- Weak spot: "Clinically proven" without a cited study was caught in only 1 of 7 runs, even though a directive names that phrase. Those numbers are in the README; a reviewer that is sometimes wrong should say so.
What I'd tell you before you build this
1. Tags are consolidation boundaries, not just filters. Decide explicitly what observations should consolidate across. observation_scopes makes that one line, once you know the default scopes by the full tag set.
2. Write memory in the order it happened. Backdate timestamps and let consolidation finish between batches. A reversal only reads as a reversal if the thing it reverses is already there.
3. One quote per document gives you citations for free. Metadata on each retained item lets every flag show the client's exact words and date. That's the difference between an opinion and evidence.
4. Don't make the model do what code can. The vision model called a peach background "white". Colours and texture are measured from pixels now.
5. Measure across runs. Reflect proposes slightly different issues each time, so I publish the real catch rate and record reviews for replay when a live run misbehaves.
If you're wondering what agent memory actually is beyond "a vector store with a nicer name", this project changed my answer. Storage was easy. The work was deciding what counts as the same thing, what order things happened in, and what the agent may forget.
The code is on GitHub: Round Zero. It uses Hindsight for memory, with Groq for vision and verification, FastAPI with server-sent events for streaming progress, and a Next.js front end.
Top comments (0)