A programmer’s walkthrough: watching the literature from the wire up — what detectors are, what they’re for, and a script you can run in the next minute.
A detector is a persistent watch on one work. A link detector collects new links that land on the work — optionally filtered by a set of link types, a direction, and who authored the link. A revision detector collects new revisions, so a reader can do what Miller called differential reading: read only what changed since they last looked. The collection is the record: hits accumulate until you ack them, and acking marks read — it never deletes.
Miller’s demo story, restated for our wire: Dan writes a marketing requirement and links it, with a requirement-type link, to the passage in Ruth’s technical plan it concerns. Ruth is not watching the phone — she has a link detector on her plan, filtered to requirement links. The link lands; the detector collects it; Ruth reads the collection at her leisure and revises the plan. John has a revision detector on the same plan; Ruth’s revision is collected for him; he compares versions and updates the budget. Three people, two detector kinds, one typed link — and nobody had to be interrupted.
| Operation | Wire code | What it does |
|---|---|---|
detector_create | 0x0f22 | Plant a watch: { work_id, kind: "links"|"revisions", match? } |
detector_list | 0x0f23 | Your detectors, each with hits since last ack and an unread count |
detector_ack | 0x0f24 | Mark a detector’s hits read (kept, never deleted) |
detector_delete | 0x0f25 | Stop watching (owner only) |
The match object for link detectors is a predicate, never a single value:
{
"link_types": [1531, 7], // a SET — empty or absent means ALL types
"direction": "in", // in | out | any
"from_clubs": [4112] // only links by these authors — absent = anyone
}
FeFillDetector matched one exact element; Miller’s link detectors matched one type each. That single-value shape is the limitation this design exists to avoid. The object is additive: span-level regions, authority filters, and endorsement ordering arrive with the fossil-engine integration without a wire break — clients treat unknown fields as “no additional filter”.The complete walkthrough lives in the repository as a runnable script — it plants both kinds, triggers each the way your code would, reads the collection, acks, and cleans up. It runs as the public session; no admin needed.
git clone https://github.com/jonesd/xudanu node examples/detectors.mjs # against a local server node examples/detectors.mjs wss://your-server/xudanu?format=json
Its narrated transcript (abbreviated):
▼ link detector #0 planted on the plan
match: {"link_types":[1531],"direction":"in"}
▼ revision detector #1 planted on the plan
▼ requirement link 0xab attached — detector fired silently
▼ a Comment link also landed — filtered OUT by the type set
▼ plan revised (duralum → titanalum) — revision detector fired
▼ the collection:
#0 links on 0x5fc — unread 1: link 0xab
#1 revisions on 0x5fc — unread 1: revision 1
acked #0: 1 hits marked read (kept, not deleted)
unread after ack: 0 (hits retained)
Link detectors fire when types are applied (link_set_types) — the final type set is what the predicate evaluates, and one link collects exactly once no matter how often types are replayed. Revision detectors fire at the single commit choke point (revise_work): every path — work_revise, work_save_and_release, work_set_text, seeding — funnels through it.
Detectors and their hits live in a detectors.json sidecar saved with every checkpoint — restarts keep the collection. Each detector keeps at most 200 hits (oldest dropped when the cap is hit). Only the owning club sees a detector and its hits.
setInterval(async () => {
const { detectors } = value(await send("detector_list"));
const unread = detectors.reduce((s, d) => s + d.unread, 0);
if (unread > 0) renderBadge(unread); // then ack on read
}, 30_000);
v1 is pull-model by design: the collection is the contract, polling is the client’s choice, and live push can ride the notification system later without changing any of the above.
RecorderQuery) carries regions, authority clubs, and endorsement filters.