FishTea
Documentation

Echoes

What a visitor did with an answer, once it took them out of the conversation.

An answer can end in a link: the shop that sells the product, the paper behind the citation, the map to the venue. Until echoes the conversation lost the visitor at that point. It knew what it had shown and not what they followed. Echoes close that gap without turning the framework into a link shortener.

A reverb, not a turn#

A turn is something said, which moves the trail's head and can be rewound to. Following a link is neither. So an echo is recorded beside the turn whose answer offered it, in conversation.echo. It names the turn, the visitor, the destination and where the visitor actually went. It never becomes a turn of its own: the head stays where it was, a rewind does not show it, and a resume does not pour it.

That makes an echo a fact about an answer: this answer, shown to this visitor, sent them to that place. It is the fact an affiliate site needs to join a sale back to the question that produced it. A publication can use it to see which sources readers trust, and a venue to see which directions people asked for.

The trail is the signature#

/echo follows a destination only when the conversation published it. The turn the link names (or, when a page keeps an older turn on screen, a recent turn on the same trail) must contain the URL, exactly, in its stored pour. That is the whole of the safety argument:

A link passed on to someone else still works, because the destination was legitimately published. It is recorded as shared, under the visitor who followed it.

Declaring an echo#

Echoes are a spec section of their own, beside notifications (spec format 1.2):

"echoes": {
  "version": "2026.11.01-1",
  "outbound": {
    "doc": "A visitor followed a product to the shop that sells it",
    "handler": "echo.outbound",          // a compute call; optional
    "analytics": "outbound_click",       // the GA4 event a shell reports; default the echo's name; false for none
    "allow": ["stranger", "known", "customer"],   // optional; absent means every persona
    "via": "redirect",                   // default
    "cases": [
      { "hosts": ["*.amazon.com"], "via": "beacon", "handler": false,
        "doc": "Amazon links go direct: reported beside the link, never redirected" }
    ]
  }
}

Transports. How the browser reaches /echo:

via the link is /echo answers
redirect /echo?echo=outbound&turn=…&to=… records, runs the handler, 302 to the destination or the handler's rewrite
beacon the destination itself navigator.sendBeacon posts the echo beside the click; records, runs the handler, 204

redirect is the default because it works without JavaScript and records every click, analytics consent or not. beacon exists for destinations that must not be reached through a redirect. Some affiliate programmes say so outright, and a handler cannot change where a beacon's visitor goes.

Cases. The first case whose hosts match the destination wins, and may change the transport, the handler (false for none) and the analytics event. *.example.com matches the domain and every subdomain; a bare host matches itself and its www.. The browser and the core use the same function to decide this (planEcho, @fishtea/spec's echo.ts, reached in the browser as @fishtea/echo), so the link a page draws and the outcome the core gives it never disagree about which case applies.

The handler is an ordinary compute call from calls. It must be compute: it decides, it does not read or write, and it runs while a visitor waits on a redirect. Its budget is its own timeoutMs, else rules.calls.timeoutMs. It receives:

{ echo, echoId, to, via, record, turnId, shape, persona, shared }

and may answer { location?, data? }. location replaces the destination: an affiliate handler adds the network's sub-id so a reported sale names this echo. data is kept with the record.

Send a network a reference, not the echo id. An echo id is mixed case with - and _, and the networks do not agree about either. Awin approves the punctuation but asks for lower case, and Rakuten's own pages contradict each other on special characters. A sub-id that comes back altered belongs to a sale that can never be joined. Derive something every network keeps intact, such as lower-case hex from a hash of the echo id. Return it in data, so the record says what was sent and a reported sale joins on data->>'subId'. A handler that throws, overruns or answers anything but an http(s) location is a note on the echo, and the visitor goes to the destination as published. An echo never strands anyone.

allow. A persona an echo does not allow is sent on as published and nothing is recorded. The link was on their screen, so an error page would protect nothing.

In the browser#

The page's <head> carries the public half of the section as window.__fishteaEchoes: transports, analytics names and host cases. A handler's name never reaches the browser.

For the operator#

What it is not, yet#