Find the four asks in here.
One message, as it arrives. Four things in it need an answer from you, and all four sit mid-paragraph, in the same voice as everything around them. Press the button to see them marked, and the section the skill adds at the bottom of every reply.
The export endpoint is wired to the queue now and the happy path passes locally. A few things worth knowing about how it ended up.
Batching turned out to be the interesting part. R2 accepts a multipart upload of any size, but the worker's memory ceiling rules out holding a full month of rows before a flush, so I settled on chunks of five thousand rows. That keeps peak memory near 40 MB on the largest tenant in the seed data, with headroom for the wide columns. The retry budget I had to guess at: three attempts on a two-second backoff, copied from the ingest path — which is idempotent, and this one is not yet.
There is a schema wrinkle.
export_job.tenant_id is plain text rather than a foreign key, which I read as
deliberate given the sharding plan, so I left it alone and added an index instead.
The migration is generated and applied nowhere. Checkpointing writes a row per chunk so
a resumed job picks up at the last committed offset, which also gives us a cheap
progress read for the dashboard later.
On naming: I kept exportJob in the service layer and
export_job in SQL, matching what the ingest module already does, so the
mapping stays boring. The queue consumer is its own module rather than a branch inside
the existing one, because the two share nothing but the R2 client.
Tests cover the chunker and the checkpoint resume. The end-to-end test is skipped, since it needs a real R2 binding — worth deciding whether CI runs it at all, at about ninety seconds and real storage per run. The unit tests run in the workers pool and take four seconds.
Before this goes to staging I need to know whether the nightly cron still points at the old bucket. If it does, applying the migration starts writing into a live path. Everything else is ready to go behind that.
NEEDS YOUR ATTENTION
export_job.tenant_id is plain text on purpose, for sharding,
and added an index rather than a foreign key.
The block did not shorten the reply. Everything above it is still there, because that is how the answer got written and no instruction reliably stops it. What changed is that acting on the reply no longer needs a full read of it.
The wall is not the mistake.
Meaning takes a map of words to carry, and the reasoning, the alternatives and the caveats are what make an answer worth trusting rather than merely confident. Cut them and you get a confident sentence with no way to check it.
The ask lands where the reasoning reached it: mid-paragraph, in the same voice as the map around it, pointing at something four messages back. Nothing marks it as the part addressed to a person.
You hunt for it, or you miss it — and when the agent asks a fourth time, the question no longer makes sense on its own, because it needed the paragraph it came out of.
So this filters rather than shortens. One section at the end holds everything that needs a person — a blocker, a decision, an assumption, a risk, a question — and reading the wall above it becomes optional.
Does a person have to see it?
That is the whole filter, and it is not the same question as how important a sentence sounds. Run it over the reply from Plate I and four lines pass. Everything else in there is worth having and needs nothing from you.
tenant_id was left as plain text on an assumption about sharding.
Two rules, and one of them carries it.
Every item stands on its own.
This is the load-bearing one. An item that points at the paragraph above is answerable only by somebody who just read the wall — the reader you were sparing. Name the thing, then ask.
Nothing. is a real answer.
An empty section says so out loud. That is what makes a quiet turn readable at a glance instead of ambiguous — the difference between nothing needed and nobody looked.
Nothing.
This is a filter, not a budget. A reply that shortens itself into ambiguity has traded one cost for a worse one.