ImageMagick sizes its pixel cache against the machine and will hold an entire
decoded image in RAM. On a host with room that is right. On a small one it is
how resizing a photograph kills whatever else is running — and "whatever else"
includes the website this service exists to serve, which is not an acceptable
way for an image to fail.
So the decode runs under `-limit memory` / `-limit map` / `-limit disk`. Past
the caps ImageMagick spills to a temporary file and carries on, which turns an
out-of-memory kill of whatever the kernel picks into a conversion that is
slower. Nobody minds a slow photograph; everybody minds a site that went down
while one was being resized.
Measured on fofos. A 12 MP HEIC peaks at 145 MB, so the 256 MB default does not
bind and the output is byte-identical — it is a ceiling, not a tax. Forced to
bind, on a 36 MP image with a 32 MiB cap, peak RSS is 12 MB and it still exits 0
with the file written.
`-limit thread 1` for the same reason: one decode is one image, and each thread
carries its own share of the cache the caps exist to bound.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`output: { webp: { quality: 82 } }` was accepted and did nothing. It reads
perfectly and mirrors how `pipeline` names an operation, so it is what a caller
writes from memory — and quietly it meant format fell through to the "webp"
default (right answer, wrong reason) while the options were never read, so the
quality asked for was not the quality encoded. Ask for `{ png: … }` the same way
and you got a WebP with a .png on the end of it.
Every other part of this API rejects a name it does not know. This one did not,
and what it let through was the kind of failure nobody finds by looking at the
output.
Also report `needsConversion` from `/v1/image`. Which formats have to go through
ImageMagick before sharp can read their pixels is this service's fact, and it is
expensive enough to plan around rather than merely absorb: 8.5 s against 1.1 s
for the resize and encode after it. A caller building a seven-rung ladder needs
to know so it can decode once itself, and a copy of the list kept on its side
would go on paying after this build stopped needing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
av-club keeps doing exactly what it did — sharp, ffmpeg and ffprobe behind one
API, and the invariant that the caller never names a path. What changes is how it
is reached and who decides who may reach it.
`src/` is `sidecar/`, because that is the name the platform looks for:
`sidecar/index.ts` present means "this plugin ships a process", and presence is
the declaration for everything structural here. `manifest.ts` at the root is the
other half of that rule.
api/router.ts is a proxy and nothing else. the platform authenticates, checks the
`av-club` permission, and forwards — so the sidecar now accepts a request
carrying `X-Officer-User` and no longer demands its own bearer token from
callers who came that way. it binds LOOPBACK to make that trustworthy, which is
the same boundary caldav's DAV door already relies on. AV_CLUB_TOKEN stays, and
still guards anything calling the port directly: fofos/platform is the stated
first consumer and is not Officer.
THREE THINGS HAD TO GO, and each one broke it in a way worth writing down.
package.json and bun.lock: a package.json inside a plugin is a package boundary,
so bun stops walking up for the platform's tsconfig and `@@/sidecar/connect`
stops resolving. the sidecar died on its first import, fifteen times. npm
dependencies go in the manifest instead, where npm-deps.ts intersects them across
plugins into one plugins/node_modules — which is also how this surfaced a real
conflict: sharp ^0.33.5 (atelier) and ^0.35.3 here do not overlap, since a 0.x
caret pins the minor. atelier moved up; it calls one method.
tsconfig.json: same failure one layer earlier, shadowing the platform's `@@/*`
paths so api/router.ts would not mount. no other plugin has one.
no screens, deliberately. this is an API and a body of documentation; its callers
are a commander watching a folder, an agent, or curl. a dock tile would advertise
a page with nothing on it.
verified through /api/av-club with an agent key: a 3000x2000 jpeg uploaded,
probed, and resized to a 1200x800 avif — 166 KB to 1 KB — without the caller ever
holding av-club's own token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043