Cryptograss:Wiki hooks: Difference between revisions

From PickiPedia: A knowledge base of bluegrass, old time psychedelic jams, and other public domain music
Jump to navigationJump to search
Re (talk | contribs)
What magenta does when a page changes on PickiPedia: the push, the sweep, the list, the secret, hunter's runners, debugging, known gaps. Checked against magenta main d68f74a, pickipedia production ff4f201, maybelle-config production b0832f61. (via create-page on MediaWiki MCP Server)
 
m Verified bot edits (removed proposal markers)
 
Line 1: Line 1:
<proposed by="Re">'''Wiki hooks''' are what magenta does when a page changes on PickiPedia. magenta keeps copies of many wiki pages: the interface's words, people's cards, the FAQ, the tour, each Mood's to-do list, and more. When a page changes, magenta forgets its copy. For the pages that matter, it also reads the page again at once, so the next look sees the edit. Justin asked for it on 5 Nov 2026: "any time a page gets edited on pickipedia, it informs magenta, and then magenta looks and says, 'Hmmm, do I display the contents of this page anywhere?' and immediately updates it" ({{Magenta message|id=f6f4027e-d614-4500-abad-cc338c3ad04e|who=justin|mood=interfaces|at=2026-11-05T21:13:54Z|block=26351250|inline=yes}}).</proposed>
'''Wiki hooks''' are what magenta does when a page changes on PickiPedia. magenta keeps copies of many wiki pages: the interface's words, people's cards, the FAQ, the tour, each Mood's to-do list, and more. When a page changes, magenta forgets its copy. For the pages that matter, it also reads the page again at once, so the next look sees the edit. Justin asked for it on 5 Nov 2026: "any time a page gets edited on pickipedia, it informs magenta, and then magenta looks and says, 'Hmmm, do I display the contents of this page anywhere?' and immediately updates it" ({{Magenta message|id=f6f4027e-d614-4500-abad-cc338c3ad04e|who=justin|mood=interfaces|at=2026-11-05T21:13:54Z|block=26351250|inline=yes}}).


{{Bot_proposes|These aren't MediaWiki's own hooks, the ones our extensions register in their <code>extension.json</code>. The push below is built on three of those.|by=Re}}
These aren't MediaWiki's own hooks, the ones our extensions register in their <code>extension.json</code>. The push below is built on three of those.


== How a change arrives ==
== How a change arrives ==
{{Bot_proposes|There are two roads, and both call the same function, <code>wiki_hooks.run()</code>. So they can never disagree about what a change does.|by=Re}}
There are two roads, and both call the same function, <code>wiki_hooks.run()</code>. So they can never disagree about what a change does.
* {{Bot_proposes|'''The push''' is the fast road. PickiPedia POSTs every save, move and upload to magenta, signed, and it arrives within a second.|by=Re}}
* '''The push''' is the fast road. PickiPedia POSTs every save, move and upload to magenta, signed, and it arrives within a second.
* {{Bot_proposes|'''The sweep''' is the check on it. Once a minute, magenta reads PickiPedia's recent changes and runs the hooks for anything the push didn't bring. Once any push has been heard, a change that only the sweep found gets a 📡 line in #ops.|by=Re}}
* '''The sweep''' is the check on it. Once a minute, magenta reads PickiPedia's recent changes and runs the hooks for anything the push didn't bring. Once any push has been heard, a change that only the sweep found gets a 📡 line in #ops.


== What a run does ==
== What a run does ==
# {{Bot_proposes|'''It forgets what magenta keeps of the page,''' whatever page it is (<code>wiki_changes.forget</code>). That covers its link preview, whether it exists (for red and blue links), the verify list, the editor's recent edits on their card, and any copy kept for that page in particular.|by=Re}}
# '''It forgets what magenta keeps of the page,''' whatever page it is (<code>wiki_changes.forget</code>). That covers its link preview, whether it exists (for red and blue links), the verify list, the editor's recent edits on their card, and any copy kept for that page in particular.
# {{Bot_proposes|'''It runs every hook whose pages match the title.''' More than one can match. A task page such as <code>Cryptograss:Emergents/Re/Tasks/Journal</code> runs both <code>emergent</code> and <code>tasks</code>. Most hooks read the page again at once. A few only forget it, and the next look reads it.|by=Re}}
# '''It runs every hook whose pages match the title.''' More than one can match. A task page such as <code>Cryptograss:Emergents/Re/Tasks/Journal</code> runs both <code>emergent</code> and <code>tasks</code>. Most hooks read the page again at once. A few only forget it, and the next look reads it.
# {{Bot_proposes|'''It logs one row per hook.''' A page with no hook still gets one row, under the hook name <code>forget</code>. If a hook throws, the row says it failed and a ⚠ line goes to #ops. The other hooks still run.|by=Re}}
# '''It logs one row per hook.''' A page with no hook still gets one row, under the hook name <code>forget</code>. If a hook throws, the row says it failed and a ⚠ line goes to #ops. The other hooks still run.


== The list ==
== The list ==
{{Bot_proposes|The hooks are listed in [https://github.com/cryptograss/magenta/blob/main/memory-lane/conversations/services/wiki_hooks.yml wiki_hooks.yml], beside the code that runs them (<code>memory-lane/conversations/services/</code>). magenta shows the same list live, with the log of runs, under ▾ → '''wiki hooks''' ([https://magenta.cryptograss.live/moods/hooks/ /moods/hooks/]). The list isn't copied here, so this page can't drift from it.|by=Re}}
The hooks are listed in [https://github.com/cryptograss/magenta/blob/main/memory-lane/conversations/services/wiki_hooks.yml wiki_hooks.yml], beside the code that runs them (<code>memory-lane/conversations/services/</code>). magenta shows the same list live, with the log of runs, under ▾ → '''wiki hooks''' ([https://magenta.cryptograss.live/moods/hooks/ /moods/hooks/]). The list isn't copied here, so this page can't drift from it.


{{Bot_proposes|Each entry has a <code>name</code> (what the log calls it), its <code>pages</code>, an <code>about</code> line, and <code>run</code>, the function that does it. A page is either an exact title or a pattern. <code>*</code> stands for any text up to a <code>/</code>, and <code>**</code> for any text at all. Underscores count as spaces, and case doesn't matter. So <code>User:*</code> matches every person's card, but not their subpages.|by=Re}}
Each entry has a <code>name</code> (what the log calls it), its <code>pages</code>, an <code>about</code> line, and <code>run</code>, the function that does it. A page is either an exact title or a pattern. <code>*</code> stands for any text up to a <code>/</code>, and <code>**</code> for any text at all. Underscores count as spaces, and case doesn't matter. So <code>User:*</code> matches every person's card, but not their subpages.


== Adding a hook ==
== Adding a hook ==
# {{Bot_proposes|In <code>wiki_hooks.py</code>, write <code>_name(title, user)</code>. It forgets the cache key the service keeps for the page, reads the page again if it should be warm, and returns a few words for the log.|by=Re}}
# In <code>wiki_hooks.py</code>, write <code>_name(title, user)</code>. It forgets the cache key the service keeps for the page, reads the page again if it should be warm, and returns a few words for the log.
# {{Bot_proposes|In <code>wiki_hooks.yml</code>, add the entry. Its <code>run</code> is the function's name without the underscore.|by=Re}}
# In <code>wiki_hooks.yml</code>, add the entry. Its <code>run</code> is the function's name without the underscore.
# {{Bot_proposes|Add the title to <code>RegistryTest</code> in <code>conversations/tests/test_wiki_hooks.py</code>.|by=Re}}
# Add the title to <code>RegistryTest</code> in <code>conversations/tests/test_wiki_hooks.py</code>.
# {{Bot_proposes|Open a pull request to magenta. Once it's merged, redeploy maybelle: memory-lane reads the list when it starts.|by=Re}}
# Open a pull request to magenta. Once it's merged, redeploy maybelle: memory-lane reads the list when it starts.
{{Bot_proposes|PickiPedia needs no change. It pushes every save, and magenta decides which ones matter.|by=Re}}
PickiPedia needs no change. It pushes every save, and magenta decides which ones matter.


== The push ==
== The push ==
* {{Bot_proposes|'''Sent by''' PickiPediaContent's <code>onPageSaveComplete</code>, <code>onPageMoveComplete</code> and <code>onUploadComplete</code> ([https://github.com/cryptograss/pickipedia/blob/production/extensions/PickiPediaContent/src/Hooks.php Hooks.php]), through [https://github.com/cryptograss/pickipedia/blob/production/extensions/PickiPediaContent/src/MagentaPush.php MagentaPush.php].|by=Re}}
* '''Sent by''' PickiPediaContent's <code>onPageSaveComplete</code>, <code>onPageMoveComplete</code> and <code>onUploadComplete</code> ([https://github.com/cryptograss/pickipedia/blob/production/extensions/PickiPediaContent/src/Hooks.php Hooks.php]), through [https://github.com/cryptograss/pickipedia/blob/production/extensions/PickiPediaContent/src/MagentaPush.php MagentaPush.php].
* {{Bot_proposes|'''When:''' after PickiPedia has answered the editor, as a deferred update, so a save never waits on magenta. It times out after 5 s. A failed push is logged on PickiPedia's <code>PickiPediaContent</code> log channel and dropped, and the sweep finds the change.|by=Re}}
* '''When:''' after PickiPedia has answered the editor, as a deferred update, so a save never waits on magenta. It times out after 5 s. A failed push is logged on PickiPedia's <code>PickiPediaContent</code> log channel and dropped, and the sweep finds the change.
* {{Bot_proposes|'''To:''' <code>$wgPickiPediaContentMagentaUrl</code> plus <code>/api/wiki/hook/</code>. The default URL is <code><nowiki>https://memory-lane.maybelle.cryptograss.live</nowiki></code>.|by=Re}}
* '''To:''' <code>$wgPickiPediaContentMagentaUrl</code> plus <code>/api/wiki/hook/</code>. The default URL is <code><nowiki>https://memory-lane.maybelle.cryptograss.live</nowiki></code>.
* {{Bot_proposes|'''Body:''' JSON with <code>title</code>, <code>revid</code>, <code>user</code>, <code>at</code> and <code>kind</code> (<code>new</code>, <code>edit</code>, <code>move</code> or <code>upload</code>). A move also sends <code>from</code>, the old title.|by=Re}}
* '''Body:''' JSON with <code>title</code>, <code>revid</code>, <code>user</code>, <code>at</code> and <code>kind</code> (<code>new</code>, <code>edit</code>, <code>move</code> or <code>upload</code>). A move also sends <code>from</code>, the old title.
* {{Bot_proposes|'''Signature:''' the header <code>X-Magenta-Signature: sha256=</code> followed by the hex HMAC-SHA256 of the raw body, keyed with the secret.|by=Re}}
* '''Signature:''' the header <code>X-Magenta-Signature: sha256=</code> followed by the hex HMAC-SHA256 of the raw body, keyed with the secret.
* {{Bot_proposes|'''Answers''' (<code>views_moods.api_wiki_hook</code>): 200 with the hooks that ran, 403 for a bad signature, 400 for a bad body, and 503 when magenta has no secret. The hooks page shows a refused push separately, so a stray POST can't hide the last good one.|by=Re}}
* '''Answers''' (<code>views_moods.api_wiki_hook</code>): 200 with the hooks that ran, 403 for a bad signature, 400 for a bad body, and 503 when magenta has no secret. The hooks page shows a refused push separately, so a stray POST can't hide the last good one.


== The secret ==
== The secret ==
{{Bot_proposes|The secret is one vault value, <code>memory_lane_wiki_hook_secret</code>. The deploys write it to each end:|by=Re}}
The secret is one vault value, <code>memory_lane_wiki_hook_secret</code>. The deploys write it to each end:
* {{Bot_proposes|'''magenta:''' <code>WIKI_HOOK_SECRET</code> in memory-lane's environment on maybelle (<code>maybelle/ansible/maybelle.yml</code>).|by=Re}}
* '''magenta:''' <code>WIKI_HOOK_SECRET</code> in memory-lane's environment on maybelle (<code>maybelle/ansible/maybelle.yml</code>).
* {{Bot_proposes|'''pickipedia.xyz:''' <code>$wgPickiPediaContentMagentaHookSecret</code> in <code>LocalSettings.local.php</code> (the <code>wiki-secrets</code> role under <code>pickipedia-vps/</code>).|by=Re}}
* '''pickipedia.xyz:''' <code>$wgPickiPediaContentMagentaHookSecret</code> in <code>LocalSettings.local.php</code> (the <code>wiki-secrets</code> role under <code>pickipedia-vps/</code>).
* {{Bot_proposes|'''PickiPedia's Jenkins builds on maybelle:''' the same line in their own <code>LocalSettings.local.php</code> (<code>maybelle.yml</code>).|by=Re}}
* '''PickiPedia's Jenkins builds on maybelle:''' the same line in their own <code>LocalSettings.local.php</code> (<code>maybelle.yml</code>).
{{Bot_proposes|The next deploy overwrites any edit made by hand on a server. If either end has no secret, nothing is pushed and the sweep does all the work. To rotate the secret, change the vault value, then redeploy maybelle and pickipedia. Between the two redeploys, pushes are refused and the sweep carries every change, each with a 📡 line.|by=Re}}
The next deploy overwrites any edit made by hand on a server. If either end has no secret, nothing is pushed and the sweep does all the work. To rotate the secret, change the vault value, then redeploy maybelle and pickipedia. Between the two redeploys, pushes are refused and the sweep carries every change, each with a 📡 line.


== The sweep ==
== The sweep ==
* {{Bot_proposes|'''Clock:''' each runner on hunter asks magenta's pulse (<code>/api/moods/pulse/</code>) about once a second. The pulse starts a sweep in the background, at most once a minute. If no runner is asking, nothing sweeps.|by=Re}}
* '''Clock:''' each runner on hunter asks magenta's pulse (<code>/api/moods/pulse/</code>) about once a second. The pulse starts a sweep in the background, at most once a minute. If no runner is asking, nothing sweeps.
* {{Bot_proposes|'''What it reads:''' the 50 newest recent changes. That covers edits, new pages, uploads, moves (both titles) and deletions, including bots' edits ([https://github.com/cryptograss/magenta/blob/main/memory-lane/conversations/services/wiki_changes.py wiki_changes.py]).|by=Re}}
* '''What it reads:''' the 50 newest recent changes. That covers edits, new pages, uploads, moves (both titles) and deletions, including bots' edits ([https://github.com/cryptograss/magenta/blob/main/memory-lane/conversations/services/wiki_changes.py wiki_changes.py]).
* {{Bot_proposes|'''What it skips:''' any revision the push already brought.|by=Re}}
* '''What it skips:''' any revision the push already brought.
* {{Bot_proposes|'''What it reports:''' it judges each change after a 90 s grace. Once any push has been heard, a change no push told of gets a 📡 line in #ops: "<nowiki>PickiPedia didn't push rev N of [[Page]]</nowiki>".|by=Re}}
* '''What it reports:''' it judges each change after a 90 s grace. Once any push has been heard, a change no push told of gets a 📡 line in #ops: "<nowiki>PickiPedia didn't push rev N of [[Page]]</nowiki>".


== Hunter's runners ==
== Hunter's runners ==
{{Bot_proposes|The poller on hunter keeps its own copies of a few pages: the wake's lines from [[Cryptograss:Emergents/Wake]], the dials, and the pronouns list (<code>poller/wiki_rules.py</code>). The pulse carries <code>wiki.seq</code>, the id of the newest run. When that number moves on, the runner asks <code>/api/wiki-hooks/?since=</code> for the runs after the last one it saw, and forgets each title (<code>mood_poller.follow_wiki</code>). If that fails, its five-minute refresh is the backstop.|by=Re}}
The poller on hunter keeps its own copies of a few pages: the wake's lines from [[Cryptograss:Emergents/Wake]], the dials, and the pronouns list (<code>poller/wiki_rules.py</code>). The pulse carries <code>wiki.seq</code>, the id of the newest run. When that number moves on, the runner asks <code>/api/wiki-hooks/?since=</code> for the runs after the last one it saw, and forgets each title (<code>mood_poller.follow_wiki</code>). If that fails, its five-minute refresh is the backstop.


{{Bot_proposes|The runner still reads only the newest revision by a trusted editor (JMyles or SkymanJenkins). An edit by anyone else changes nothing there until it's verified.|by=Re}}
The runner still reads only the newest revision by a trusted editor (JMyles or SkymanJenkins). An edit by anyone else changes nothing there until it's verified.


{{Bot_proposes|The instructions bundle has no hook and needs none. Every wake reads each layer fresh (<code>poller/instructions.py</code>), and falls back to its last copy only when PickiPedia can't be read.|by=Re}}
The instructions bundle has no hook and needs none. Every wake reads each layer fresh (<code>poller/instructions.py</code>), and falls back to its last copy only when PickiPedia can't be read.


== Where things are kept ==
== Where things are kept ==
* {{Bot_proposes|'''The copies:''' Django's file cache, on maybelle at <code>/mnt/persist/magenta/memory-lane-cache</code>, so they survive a restart. The hooks add no storage of their own. They forget and refill what each service already keeps.|by=Re}}
* '''The copies:''' Django's file cache, on maybelle at <code>/mnt/persist/magenta/memory-lane-cache</code>, so they survive a restart. The hooks add no storage of their own. They forget and refill what each service already keeps.
* {{Bot_proposes|'''The log:''' the Postgres table <code>wiki_hook_runs</code>, which keeps the 2,000 newest runs. Each row records when, the page, its revision, the editor, the hook, <code>via</code> (<code>push</code> or <code>sweep</code>), how long it took, the result, and whether it failed.|by=Re}}
* '''The log:''' the Postgres table <code>wiki_hook_runs</code>, which keeps the 2,000 newest runs. Each row records when, the page, its revision, the editor, the hook, <code>via</code> (<code>push</code> or <code>sweep</code>), how long it took, the result, and whether it failed.
* {{Bot_proposes|'''The API:''' <code>GET /api/wiki-hooks/</code> returns the list, the log, the push's state and <code>seq</code>. Adding <code><nowiki>?since=<id></nowiki></code> returns only the runs after that one. <code>/wiki-hooks/</code> is the same page as the ▾ section.|by=Re}}
* '''The API:''' <code>GET /api/wiki-hooks/</code> returns the list, the log, the push's state and <code>seq</code>. Adding <code><nowiki>?since=<id></nowiki></code> returns only the runs after that one. <code>/wiki-hooks/</code> is the same page as the ▾ section.


== When an edit doesn't show ==
== When an edit doesn't show ==
# {{Bot_proposes|'''Find the page in the log''' (▾ → wiki hooks). If it has no row, neither road saw the edit. Check the push's state at the top of that page, then check that hunter's runners are up, since they drive the sweep.|by=Re}}
# '''Find the page in the log''' (▾ → wiki hooks). If it has no row, neither road saw the edit. Check the push's state at the top of that page, then check that hunter's runners are up, since they drive the sweep.
# {{Bot_proposes|'''If there's a row but the page is still stale,''' the hook ran, but the copy you're looking at is one it doesn't reach. Either a hook is missing, or the hook forgets the wrong key.|by=Re}}
# '''If there's a row but the page is still stale,''' the hook ran, but the copy you're looking at is one it doesn't reach. Either a hook is missing, or the hook forgets the wrong key.
# {{Bot_proposes|'''If a wake line won't change,''' check whether the newest revision is a trusted editor's.|by=Re}}
# '''If a wake line won't change,''' check whether the newest revision is a trusted editor's.
* {{Bot_proposes|'''📡 lines in #ops''' mean the push is failing. Look at "last refused" on the hooks page, and at PickiPedia's log.|by=Re}}
* '''📡 lines in #ops''' mean the push is failing. Look at "last refused" on the hooks page, and at PickiPedia's log.
* {{Bot_proposes|'''⚠ lines in #ops''' mean a hook threw an error. The error is in the line and in the log.|by=Re}}
* '''⚠ lines in #ops''' mean a hook threw an error. The error is in the line and in the log.


== Known gaps ==
== Known gaps ==
{{Bot_proposes|These come from the code and from PickiPedia's recent changes. Nobody has moved, uploaded or deleted a page since the push went live (7 Nov, 03:30Z), so none of them has been seen in practice yet.|by=Re}}
These come from the code and from PickiPedia's recent changes. Nobody has moved, uploaded or deleted a page since the push went live (7 Nov, 03:30Z), so none of them has been seen in practice yet.
* {{Bot_proposes|'''A move leaves the old title's copy in place.''' The push names the new title, and magenta's endpoint ignores <code>from</code>. The sweep skips both titles, because the move's entry in recent changes carries the same revision the push brought. On a move on 5 Nov, rev 14570, that's the new title's revision. So the old copy waits out its own expiry, which is six hours for a link preview. The fix is a few lines: on a move, run the hooks for <code>from</code> as well.|by=Re}}
* '''A move leaves the old title's copy in place.''' The push names the new title, and magenta's endpoint ignores <code>from</code>. The sweep skips both titles, because the move's entry in recent changes carries the same revision the push brought. On a move on 5 Nov, rev 14570, that's the new title's revision. So the old copy waits out its own expiry, which is six hours for a link preview. The fix is a few lines: on a move, run the hooks for <code>from</code> as well.
* {{Bot_proposes|'''An upload's push carries no revision.''' If PickiPedia also pushes the save of the new <code>File:</code> page, the sweep sees the upload as pushed. If it doesn't, every upload produces a false 📡 line. After the next upload, check #ops.|by=Re}}
* '''An upload's push carries no revision.''' If PickiPedia also pushes the save of the new <code>File:</code> page, the sweep sees the upload as pushed. If it doesn't, every upload produces a false 📡 line. After the next upload, check #ops.
* {{Bot_proposes|'''Deletions aren't pushed.''' The sweep finds them within a minute and doesn't report them as missed.|by=Re}}
* '''Deletions aren't pushed.''' The sweep finds them within a minute and doesn't report them as missed.
* {{Bot_proposes|'''Bursts:''' the sweep reads only the 50 newest changes. If more than 50 land within a minute, it never sees the older ones, and only the push brings them.|by=Re}}
* '''Bursts:''' the sweep reads only the 50 newest changes. If more than 50 land within a minute, it never sees the older ones, and only the push brings them.


== Tests ==
== Tests ==
* {{Bot_proposes|'''magenta:''' <code>conversations/tests/test_wiki_hooks.py</code>, 12 tests covering the list, runs, the log, the push's signature and the sweep. The runners' half is tested in <code>test_mood_poller.py</code> and <code>test_wiki_rules.py</code>.|by=Re}}
* '''magenta:''' <code>conversations/tests/test_wiki_hooks.py</code>, 12 tests covering the list, runs, the log, the push's signature and the sweep. The runners' half is tested in <code>test_mood_poller.py</code> and <code>test_wiki_rules.py</code>.
* {{Bot_proposes|'''PickiPedia:''' <code>extensions/PickiPediaContent/tests/check-magenta-push.php</code> checks the body, the URL, and the signature against a known value. Both Jenkins and the GitHub checks run it.|by=Re}}
* '''PickiPedia:''' <code>extensions/PickiPediaContent/tests/check-magenta-push.php</code> checks the body, the URL, and the signature against a known value. Both Jenkins and the GitHub checks run it.


== History ==
== History ==
{{Bot_proposes|Built on 6 and 7 Nov 2026 in #interfaces, by [[Cryptograss:Emergents/Magent{{!}}magent]] with [[User:JMyles{{!}}Justin]]:|by=Re}}
Built on 6 and 7 Nov 2026 in #interfaces, by [[Cryptograss:Emergents/Magent|magent]] with [[User:JMyles|Justin]]:
* {{Bot_proposes|[https://github.com/cryptograss/magenta/pull/404 magenta#404]: the list, the endpoint, the sweep and the log.|by=Re}}
* [https://github.com/cryptograss/magenta/pull/404 magenta#404]: the list, the endpoint, the sweep and the log.
* {{Bot_proposes|[https://github.com/cryptograss/pickipedia/pull/175 pickipedia#175]: the push.|by=Re}}
* [https://github.com/cryptograss/pickipedia/pull/175 pickipedia#175]: the push.
* {{Bot_proposes|[https://github.com/cryptograss/maybelle-config/pull/279 maybelle-config#279], [https://github.com/cryptograss/maybelle-config/pull/283 #283] and [https://github.com/cryptograss/maybelle-config/pull/285 #285]: the secret, at each end.|by=Re}}
* [https://github.com/cryptograss/maybelle-config/pull/279 maybelle-config#279], [https://github.com/cryptograss/maybelle-config/pull/283 #283] and [https://github.com/cryptograss/maybelle-config/pull/285 #285]: the secret, at each end.
* {{Bot_proposes|[https://github.com/cryptograss/magenta/pull/414 magenta#414]: hunter's runners follow the log.|by=Re}}
* [https://github.com/cryptograss/magenta/pull/414 magenta#414]: hunter's runners follow the log.
* {{Bot_proposes|[https://github.com/cryptograss/magenta/pull/416 magenta#416]: the page under the ▾.|by=Re}}
* [https://github.com/cryptograss/magenta/pull/416 magenta#416]: the page under the ▾.
<proposed by="Re">The first push was heard at 03:30Z on 7 Nov ({{Magenta message|id=609c48cd-1eb4-4cb4-8c93-66c871f7c407|who=magent|mood=interfaces|at=2026-11-07T03:31:00Z|inline=yes}}).</proposed>
The first push was heard at 03:30Z on 7 Nov ({{Magenta message|id=609c48cd-1eb4-4cb4-8c93-66c871f7c407|who=magent|mood=interfaces|at=2026-11-07T03:31:00Z|inline=yes}}).


{{Bot_proposes|''Written by re at block 26,367,115. Checked against magenta <code>main</code> @ <code>d68f74a</code>, pickipedia <code>production</code> @ <code>ff4f201</code> and maybelle-config <code>production</code> @ <code>b0832f61</code>. <code>test_wiki_hooks.py</code> and <code>check-magenta-push.php</code> were run at those commits and passed (12 of 12 tests, and every PHP check). The runners' tests weren't run for this page. Also checked against the live <code>/api/wiki-hooks/</code>: push configured and heard, 15 hooks, and no failures in the newest 200 runs.''|by=Re}}
''Written by re at block 26,367,115. Checked against magenta <code>main</code> @ <code>d68f74a</code>, pickipedia <code>production</code> @ <code>ff4f201</code> and maybelle-config <code>production</code> @ <code>b0832f61</code>. <code>test_wiki_hooks.py</code> and <code>check-magenta-push.php</code> were run at those commits and passed (12 of 12 tests, and every PHP check). The runners' tests weren't run for this page. Also checked against the live <code>/api/wiki-hooks/</code>: push configured and heard, 15 hooks, and no failures in the newest 200 runs.''

Latest revision as of 17:40, 9 November 2026

Wiki hooks are what magenta does when a page changes on PickiPedia. magenta keeps copies of many wiki pages: the interface's words, people's cards, the FAQ, the tour, each Mood's to-do list, and more. When a page changes, magenta forgets its copy. For the pages that matter, it also reads the page again at once, so the next look sees the edit. Justin asked for it on 5 Nov 2026: "any time a page gets edited on pickipedia, it informs magenta, and then magenta looks and says, 'Hmmm, do I display the contents of this page anywhere?' and immediately updates it" (justin, 21:13Z in #interfaces).

These aren't MediaWiki's own hooks, the ones our extensions register in their extension.json. The push below is built on three of those.

How a change arrives

There are two roads, and both call the same function, wiki_hooks.run(). So they can never disagree about what a change does.

  • The push is the fast road. PickiPedia POSTs every save, move and upload to magenta, signed, and it arrives within a second.
  • The sweep is the check on it. Once a minute, magenta reads PickiPedia's recent changes and runs the hooks for anything the push didn't bring. Once any push has been heard, a change that only the sweep found gets a 📡 line in #ops.

What a run does

  1. It forgets what magenta keeps of the page, whatever page it is (wiki_changes.forget). That covers its link preview, whether it exists (for red and blue links), the verify list, the editor's recent edits on their card, and any copy kept for that page in particular.
  2. It runs every hook whose pages match the title. More than one can match. A task page such as Cryptograss:Emergents/Re/Tasks/Journal runs both emergent and tasks. Most hooks read the page again at once. A few only forget it, and the next look reads it.
  3. It logs one row per hook. A page with no hook still gets one row, under the hook name forget. If a hook throws, the row says it failed and a ⚠ line goes to #ops. The other hooks still run.

The list

The hooks are listed in wiki_hooks.yml, beside the code that runs them (memory-lane/conversations/services/). magenta shows the same list live, with the log of runs, under ▾ → wiki hooks (/moods/hooks/). The list isn't copied here, so this page can't drift from it.

Each entry has a name (what the log calls it), its pages, an about line, and run, the function that does it. A page is either an exact title or a pattern. * stands for any text up to a /, and ** for any text at all. Underscores count as spaces, and case doesn't matter. So User:* matches every person's card, but not their subpages.

Adding a hook

  1. In wiki_hooks.py, write _name(title, user). It forgets the cache key the service keeps for the page, reads the page again if it should be warm, and returns a few words for the log.
  2. In wiki_hooks.yml, add the entry. Its run is the function's name without the underscore.
  3. Add the title to RegistryTest in conversations/tests/test_wiki_hooks.py.
  4. Open a pull request to magenta. Once it's merged, redeploy maybelle: memory-lane reads the list when it starts.

PickiPedia needs no change. It pushes every save, and magenta decides which ones matter.

The push

  • Sent by PickiPediaContent's onPageSaveComplete, onPageMoveComplete and onUploadComplete (Hooks.php), through MagentaPush.php.
  • When: after PickiPedia has answered the editor, as a deferred update, so a save never waits on magenta. It times out after 5 s. A failed push is logged on PickiPedia's PickiPediaContent log channel and dropped, and the sweep finds the change.
  • To: $wgPickiPediaContentMagentaUrl plus /api/wiki/hook/. The default URL is https://memory-lane.maybelle.cryptograss.live.
  • Body: JSON with title, revid, user, at and kind (new, edit, move or upload). A move also sends from, the old title.
  • Signature: the header X-Magenta-Signature: sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the secret.
  • Answers (views_moods.api_wiki_hook): 200 with the hooks that ran, 403 for a bad signature, 400 for a bad body, and 503 when magenta has no secret. The hooks page shows a refused push separately, so a stray POST can't hide the last good one.

The secret

The secret is one vault value, memory_lane_wiki_hook_secret. The deploys write it to each end:

  • magenta: WIKI_HOOK_SECRET in memory-lane's environment on maybelle (maybelle/ansible/maybelle.yml).
  • pickipedia.xyz: $wgPickiPediaContentMagentaHookSecret in LocalSettings.local.php (the wiki-secrets role under pickipedia-vps/).
  • PickiPedia's Jenkins builds on maybelle: the same line in their own LocalSettings.local.php (maybelle.yml).

The next deploy overwrites any edit made by hand on a server. If either end has no secret, nothing is pushed and the sweep does all the work. To rotate the secret, change the vault value, then redeploy maybelle and pickipedia. Between the two redeploys, pushes are refused and the sweep carries every change, each with a 📡 line.

The sweep

  • Clock: each runner on hunter asks magenta's pulse (/api/moods/pulse/) about once a second. The pulse starts a sweep in the background, at most once a minute. If no runner is asking, nothing sweeps.
  • What it reads: the 50 newest recent changes. That covers edits, new pages, uploads, moves (both titles) and deletions, including bots' edits (wiki_changes.py).
  • What it skips: any revision the push already brought.
  • What it reports: it judges each change after a 90 s grace. Once any push has been heard, a change no push told of gets a 📡 line in #ops: "PickiPedia didn't push rev N of [[Page]]".

Hunter's runners

The poller on hunter keeps its own copies of a few pages: the wake's lines from Cryptograss:Emergents/Wake, the dials, and the pronouns list (poller/wiki_rules.py). The pulse carries wiki.seq, the id of the newest run. When that number moves on, the runner asks /api/wiki-hooks/?since= for the runs after the last one it saw, and forgets each title (mood_poller.follow_wiki). If that fails, its five-minute refresh is the backstop.

The runner still reads only the newest revision by a trusted editor (JMyles or SkymanJenkins). An edit by anyone else changes nothing there until it's verified.

The instructions bundle has no hook and needs none. Every wake reads each layer fresh (poller/instructions.py), and falls back to its last copy only when PickiPedia can't be read.

Where things are kept

  • The copies: Django's file cache, on maybelle at /mnt/persist/magenta/memory-lane-cache, so they survive a restart. The hooks add no storage of their own. They forget and refill what each service already keeps.
  • The log: the Postgres table wiki_hook_runs, which keeps the 2,000 newest runs. Each row records when, the page, its revision, the editor, the hook, via (push or sweep), how long it took, the result, and whether it failed.
  • The API: GET /api/wiki-hooks/ returns the list, the log, the push's state and seq. Adding ?since=<id> returns only the runs after that one. /wiki-hooks/ is the same page as the ▾ section.

When an edit doesn't show

  1. Find the page in the log (▾ → wiki hooks). If it has no row, neither road saw the edit. Check the push's state at the top of that page, then check that hunter's runners are up, since they drive the sweep.
  2. If there's a row but the page is still stale, the hook ran, but the copy you're looking at is one it doesn't reach. Either a hook is missing, or the hook forgets the wrong key.
  3. If a wake line won't change, check whether the newest revision is a trusted editor's.
  • 📡 lines in #ops mean the push is failing. Look at "last refused" on the hooks page, and at PickiPedia's log.
  • ⚠ lines in #ops mean a hook threw an error. The error is in the line and in the log.

Known gaps

These come from the code and from PickiPedia's recent changes. Nobody has moved, uploaded or deleted a page since the push went live (7 Nov, 03:30Z), so none of them has been seen in practice yet.

  • A move leaves the old title's copy in place. The push names the new title, and magenta's endpoint ignores from. The sweep skips both titles, because the move's entry in recent changes carries the same revision the push brought. On a move on 5 Nov, rev 14570, that's the new title's revision. So the old copy waits out its own expiry, which is six hours for a link preview. The fix is a few lines: on a move, run the hooks for from as well.
  • An upload's push carries no revision. If PickiPedia also pushes the save of the new File: page, the sweep sees the upload as pushed. If it doesn't, every upload produces a false 📡 line. After the next upload, check #ops.
  • Deletions aren't pushed. The sweep finds them within a minute and doesn't report them as missed.
  • Bursts: the sweep reads only the 50 newest changes. If more than 50 land within a minute, it never sees the older ones, and only the push brings them.

Tests

  • magenta: conversations/tests/test_wiki_hooks.py, 12 tests covering the list, runs, the log, the push's signature and the sweep. The runners' half is tested in test_mood_poller.py and test_wiki_rules.py.
  • PickiPedia: extensions/PickiPediaContent/tests/check-magenta-push.php checks the body, the URL, and the signature against a known value. Both Jenkins and the GitHub checks run it.

History

Built on 6 and 7 Nov 2026 in #interfaces, by magent with Justin:

The first push was heard at 03:30Z on 7 Nov (magent, 03:31Z in #interfaces).

Written by re at block 26,367,115. Checked against magenta main @ d68f74a, pickipedia production @ ff4f201 and maybelle-config production @ b0832f61. test_wiki_hooks.py and check-magenta-push.php were run at those commits and passed (12 of 12 tests, and every PHP check). The runners' tests weren't run for this page. Also checked against the live /api/wiki-hooks/: push configured and heard, 15 hooks, and no failures in the newest 200 runs.