computer_page_frame holds one screenshot per page a Bot opens, up to 4 MB each, so a conversation read back later shows what it was looking at. page-frames.ts:57-59 says why they need a reaper: "a Bot that browses grows this table for as long as it runs and nothing ever took anything out of it. Screenshots are the largest thing this deployment stores."
That reaper exists and works. It has one caller, and that caller runs in one deployment shape out of four.
What it looks like
The only caller is server/scripts/cull-idle-computers.ts:87. Run it under each shape this repository ships:
Compose (docker/supervisor) error: The culler only has something to do where each Bot has its own
computer, and this deployment uses the "docker" provider.
all-in-one (embedded) error: ... uses the "shared" provider.
helm computers.mode: shared error: ... uses the "shared" provider.
helm computers.mode: sandbox (runs)
charts/openbot/values.yaml:178 sets computers.mode: shared, and four of the five shipped ci/ targets use it. grep -rn "cull\|sweep\|purge" docker-compose.yml docker/s6/ returns nothing, so neither Compose nor the all-in-one image runs it either.
What that costs, against a real database, one Bot browsing forty pages a day for ninety days at 200 KB a frame:
None of it will ever be removed in those three deployments.
Why it happens
The write path is ungated: server/src/index.ts builds the store unconditionally and hands it to the gateway and to createApp, so every deployment writes frames.
The removal path is gated twice. cull-idle-computers.ts:31 throws unless the provider is sandbox, and the only thing that schedules the script is charts/openbot/templates/computer/culler-cronjob.yaml:1, which renders only in that mode.
The placement was deliberate, and the script says why:
Here rather than in the API server because this is already the sweep that runs on a schedule with a claim under it, and a second timer would be a second thing to get wrong.
The reasoning is sound. The consequence was that the sweep exists in one mode and the frames are written in all of them.
Reproduction
- Point
DATABASE_URL at a migrated database, write a frame through createPageFrameStore(...).save(...), and age its captured_at past thirty days.
- Run
bun scripts/cull-idle-computers.ts with COMPUTER_SUPERVISOR_URL set, which is the Compose shape. It refuses with the message above; the row remains.
- Run the server with
AUDIT_RETENTION_DAYS unset, which is the default, and leave it. The row remains, because nothing in the server touches this table.
Why it matters
These are the largest rows the product stores, and the deployments that never sweep them are the ones most likely to be on a single disk: Compose on somebody's box, and the all-in-one image with EMBEDDED_POSTGRES=on, where the database is inside the container. Nothing reports it — no log line, no metric — so it is invisible until somebody looks at disk usage or a backup starts overrunning its window.
What a fix probably has to do
The sweep has to live somewhere every deployment has, and the server is the only such place: the culler is Kubernetes-only by construction and Compose has no scheduler at all. The server already runs this exact shape for the audit trail — an hourly unref'd timer with an advisory lock so one replica sweeps — so frames could ride that timer rather than starting the second one the script was right to avoid.
One decision worth making rather than inheriting: audit retention is off unless AUDIT_RETENTION_DAYS is set, because deleting a trail on a default would be worse than keeping it. Screenshots are not the trail — the store already chose thirty days and the sandbox deployments have been enforcing it — so whether the frame sweep is on by default is a separate question, and answering it by reusing the audit flag would leave most deployments where they are now.
Severity
Nothing is broken and no boundary is crossed. It is unbounded growth in the table holding the largest objects in the product, in three of the four ways this repository can be deployed, with the fix already written and called from one place.
Not urgent for anyone on computers.mode: sandbox, which has been sweeping correctly all along.
computer_page_frameholds one screenshot per page a Bot opens, up to 4 MB each, so a conversation read back later shows what it was looking at.page-frames.ts:57-59says why they need a reaper: "a Bot that browses grows this table for as long as it runs and nothing ever took anything out of it. Screenshots are the largest thing this deployment stores."That reaper exists and works. It has one caller, and that caller runs in one deployment shape out of four.
What it looks like
The only caller is
server/scripts/cull-idle-computers.ts:87. Run it under each shape this repository ships:charts/openbot/values.yaml:178setscomputers.mode: shared, and four of the five shippedci/targets use it.grep -rn "cull\|sweep\|purge" docker-compose.yml docker/s6/returns nothing, so neither Compose nor the all-in-one image runs it either.What that costs, against a real database, one Bot browsing forty pages a day for ninety days at 200 KB a frame:
None of it will ever be removed in those three deployments.
Why it happens
The write path is ungated:
server/src/index.tsbuilds the store unconditionally and hands it to the gateway and tocreateApp, so every deployment writes frames.The removal path is gated twice.
cull-idle-computers.ts:31throws unless the provider issandbox, and the only thing that schedules the script ischarts/openbot/templates/computer/culler-cronjob.yaml:1, which renders only in that mode.The placement was deliberate, and the script says why:
The reasoning is sound. The consequence was that the sweep exists in one mode and the frames are written in all of them.
Reproduction
DATABASE_URLat a migrated database, write a frame throughcreatePageFrameStore(...).save(...), and age itscaptured_atpast thirty days.bun scripts/cull-idle-computers.tswithCOMPUTER_SUPERVISOR_URLset, which is the Compose shape. It refuses with the message above; the row remains.AUDIT_RETENTION_DAYSunset, which is the default, and leave it. The row remains, because nothing in the server touches this table.Why it matters
These are the largest rows the product stores, and the deployments that never sweep them are the ones most likely to be on a single disk: Compose on somebody's box, and the all-in-one image with
EMBEDDED_POSTGRES=on, where the database is inside the container. Nothing reports it — no log line, no metric — so it is invisible until somebody looks at disk usage or a backup starts overrunning its window.What a fix probably has to do
The sweep has to live somewhere every deployment has, and the server is the only such place: the culler is Kubernetes-only by construction and Compose has no scheduler at all. The server already runs this exact shape for the audit trail — an hourly unref'd timer with an advisory lock so one replica sweeps — so frames could ride that timer rather than starting the second one the script was right to avoid.
One decision worth making rather than inheriting: audit retention is off unless
AUDIT_RETENTION_DAYSis set, because deleting a trail on a default would be worse than keeping it. Screenshots are not the trail — the store already chose thirty days and the sandbox deployments have been enforcing it — so whether the frame sweep is on by default is a separate question, and answering it by reusing the audit flag would leave most deployments where they are now.Severity
Nothing is broken and no boundary is crossed. It is unbounded growth in the table holding the largest objects in the product, in three of the four ways this repository can be deployed, with the fix already written and called from one place.
Not urgent for anyone on
computers.mode: sandbox, which has been sweeping correctly all along.