Skip to content

fix: an hour is not a latency, it is a wall - #349

Merged
chubes4 merged 1 commit into
mainfrom
reconcile-latency
Aug 7, 2026
Merged

fix: an hour is not a latency, it is a wall#349
chubes4 merged 1 commit into
mainfrom
reconcile-latency

Conversation

@chubes4

@chubes4 chubes4 commented Aug 7, 2026

Copy link
Copy Markdown
Member

The hourly sweep was the wrong instrument, and you were right that it's absurd on its face.

The problem with what I shipped

A scaffolded plugin fires no WordPress lifecycle hook — no install, no activation, just a directory appearing. I covered that with an hourly sweep.

So the agent could write a plugin and be unable to edit it for up to 59 minutes, with no way to tell the person who asked which it would be. Sometimes instant, sometimes an hour. That's the friction managed hosting exists to remove, reintroduced by the mechanism meant to remove it.

Two deterministic triggers

WP-CLI after_invokewp scaffold plugin is how a plugin actually gets created, and this fires in the same command that made the directory. The reconcile lands before the agent's next tool call: no race, no perceptible delay.

Every WordPress bootstrap (init, priority 5) — covers a directory created by any means, including a bare mkdir from the agent's shell, which no cooperative hook can catch. On an active agent the next request is seconds away.

The hourly sweep stays as a backstop for a site nobody is touching.

The cost, measured not assumed

Putting a filesystem scan on init would be a worse bug than the one I'm fixing. So I measured it on h44:

inventory_hash:                    0.16 ms
full reconcile (unchanged path):   0.03 ms   (get_plugins() caches per-request)
homepage render:                 191    ms

0.08% overhead. The fingerprint short-circuit is what makes the bootstrap hook affordable — without it this wouldn't be defensible.

Note

I did go looking for whether OpenCode re-reads opencode.json mid-session, since that would bound the latency regardless of how fast the reconcile is. Searching the bundled binary was a dead end and I stopped rather than burn time reverse-engineering it. It's worth confirming separately — if OpenCode snapshots config at session start, the remaining latency is a session boundary rather than anything this code controls.

A plugin the agent scaffolds fires no WordPress lifecycle hook — no
install, no activation, just a directory appearing — and the first version
handled that with an hourly sweep. So the agent could write a plugin and
then be unable to edit it for up to fifty-nine minutes, with no way to tell
the person who asked whether it would be instant or not. That is the
friction managed hosting exists to remove, reintroduced by the mechanism
meant to remove it.

Two deterministic triggers now do the work and the sweep becomes a backstop.

WP-CLI after_invoke, because wp scaffold plugin is how a plugin actually
gets created. It fires in the SAME command that made the directory, so the
reconcile lands before the agent's next tool call: no race and no
perceptible delay.

And every WordPress bootstrap. That covers a directory created by any means,
including a bare mkdir from the agent's shell, which no cooperative hook can
catch. On an active agent the next request is seconds away.

The bootstrap hook is only affordable because the inventory fingerprint
short-circuits it. Measured on h44lacrosse.com rather than assumed: 0.16 ms
against a 191 ms page render, 0.08% overhead. Putting an unguarded
filesystem scan on init would have been a worse bug than the one being
fixed.
@chubes4
chubes4 merged commit 2179a56 into main Aug 7, 2026
45 checks passed
chubes4 added a commit that referenced this pull request Aug 7, 2026
The deterministic triggers landed in #349 and did not work. Verified by
scaffolding a plugin on h44lacrosse.com and checking, rather than trusting
that a green test meant a working feature. Three failures, each hidden
behind the last, none visible to a stubbed test.

`wp scaffold plugin` never loads WordPress. Confirmed with --debug: zero
bootstrap steps, yet the hook fires and names the callback. WP-CLI includes
the mu-plugin far enough to register the hook and no further, so the
callback ran with no get_option() and the reconcile bailed out. The
callback now loads WordPress itself when the command did not.

get_plugins() caches its directory scan for the life of the process. That is
right for a web request and a trap for CLI: WordPress loaded BEFORE the
scaffold created the directory, so the cached list had no new plugin, the
inventory hash came out unchanged, and the reconcile returned early having
seen nothing. The cache is now busted only by callers that know the
filesystem just changed, so the per-request path keeps its cached scan and
its 0.16 ms.

The reconcile broke the file it needs to write. file_put_contents()+rename()
creates a NEW inode owned by whoever ran it at their umask, so a reconcile
run as root during an upgrade replaced opencode.json with a root:root 0644
file. The runtime user could no longer write it, and every LATER reconcile
silently failed to update the permission surface — while still updating the
option and the manifest, so everything looked healthy. Writes now restore
0664 and the directory's group, mirroring service_file_normalize_perms.

That third one is the shape worth remembering: self-inflicted, one-way, and
invisible because the other two thirds of the reconcile kept working.

verify.sh gains the check that would have caught it. It asserted the
manifest was readable and the manifest directory writable, and never asked
whether the file holding the permissions could still be written.

Measured end to end on the live site: scaffold, and the plugin is editable
in the same command, with opencode.json still 664 and group-writable
afterwards.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant