fix: an hour is not a latency, it is a wall - #349
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_invoke—wp scaffold pluginis 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 baremkdirfrom 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
initwould be a worse bug than the one I'm fixing. So I measured it on h44: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.jsonmid-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.