Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .agents/repo.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,11 +615,14 @@ xlsx,odt}.ts`) still reach the catch-all as `failed` + `indexer_error` and are r
refusing every delivery reads as never called (2026-09, round i). The reference says so.
Paying it down means a `delivery_refused` skip reason stamped from the webhook door beside
`start_refused`.
- **A page is fetched three to four times a scan** — the plain probe, the render pass and, for
the homepage, a create-time metadata probe. The robots parser now honours group selection by
product token (`User-agent: TaleBot` > `*`), `Allow` with longest-match precedence, `$`
end-anchors and `Crawl-delay` (`lib/knowledge/crawl-parse.ts`, 2026-09 round J); paying down the
remaining fetch count means one fetch per page per scan.
- **A changed page is fetched twice a scan** — the plain probe and the render pass — and the
homepage once more at create, for its metadata. A page that did not change is asked for once
and not rendered (2026-10: the probe's validators and the text of its plain HTML decide,
`core/knowledge/crawl_action.ts`); a page built by its JavaScript is still rendered on every
scan. The robots parser honours group selection by product token (`User-agent: TaleBot` >
`*`), `Allow` with longest-match precedence, `$` end-anchors and `Crawl-delay`
(`lib/knowledge/crawl-parse.ts`, 2026-09 round J). Paying down the rest means one fetch per
changed page: a server-rendered page read from the probe's own bytes, without the browser.
- **A cancelled run answers `trace: null` and `effects: null`** where a failed run answers both,
so what a cancel did not undo is readable only through `checkpoints` (2026-09, round i).
Paying it down means keeping the partial trace the way the failed path does.
Expand Down
2 changes: 2 additions & 0 deletions docs/de/platform/knowledge/crawling.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Bei einer ganzen Website nutzt der Crawler Startseite und veröffentlichte Sitem

Scans arbeiten schrittweise: Unveränderte Inhalte werden übersprungen, geänderte erneut indexiert, neue Seiten hinzugefügt und entfernte aus dem Index genommen — ebenso Seiten, die die `robots.txt` inzwischen verbietet. Eine Seite, die auf eine andere Adresse derselben Website weiterleitet, wird nur einmal indexiert: unter der Adresse, auf der sie landet. Die Seitenzähler der Zeile folgen dem Scan, während Seiten landen, nach dem Entdecken und nach jedem gespeicherten Batch, die Tabelle bewegt sich also, während ein Scan läuft. Eine URL-Liste aktualisiert ihre feste Auswahl nach demselben Zeitplan. Nach erfolgreicher Indexierung ist keine gesonderte Veröffentlichung nötig.

Ein Scan fragt jede Seite einmal ab. Dabei schickt er mit, was der Server der Website beim letzten Besuch über die Seite angegeben hat; antwortet der Server mit `304 Not Modified`, wird die Seite nicht erneut übertragen. Viele Server erzeugen ihre Seiten bei jeder Anfrage neu und nennen keinen Änderungszeitpunkt; dann vergleicht der Crawler den Text der gelieferten Seite. In beiden Fällen wird eine unveränderte Seite nicht im Browser geöffnet, ihre Bilder, Skripte und Stylesheets werden also nicht geladen. Die Ausnahme ist eine Seite, deren Inhalt erst ihr eigenes JavaScript aufbaut: Nur ein Browser kann sie lesen, deshalb wird sie bei jedem Scan gerendert.

Der Crawler besucht die Seiten ohne Anmeldung. Eine URL macht private Inhalte nicht zugänglich. Bei jeder Anfrage stellt er sich als `TaleBot/<version> (+https://docs.tale.dev/platform/knowledge/crawling)` vor, sodass eine `robots.txt`-Gruppe ihn beim Namen nennen kann — `User-agent: TaleBot` —, um allein ihn zu erlauben, zu drosseln oder abzuweisen.

Der Crawler hält sich an die `Disallow`-Regeln der `robots.txt` für den Agenten `*` auf jedem Weg, über den eine URL hereinkommen kann — die Sitemaps, der Linklauf und die Links, die eine gerenderte JavaScript-Seite preisgibt — und noch einmal vor jedem Abruf: Eine Seite, die eine Regel abdeckt, wird nie geholt, und eine Seite, die eine später hinzugekommene Regel abdeckt, verlässt den Index beim nächsten Scan. Ausdrücklich angegebene URLs filtern die Regeln nicht: Eine gelistete Adresse ist deine Anweisung. Liefert ein Abruf den HTTP-Header `X-Robots-Tag: noindex` oder `none` oder trägt die Seite ein HTML-Tag `<meta name="robots" content="noindex">`, wird der Inhalt nicht indexiert — auch bei einer URL-Liste —, und was ein früherer Scan von der Seite gespeichert hat, fällt weg. Diese Regeln sind Höflichkeit, kein Zugriffsschutz: Wenn du die Quellwebsite verwaltest, verlass dich nicht auf den Crawler als Zugangskontrolle.
Expand Down
2 changes: 2 additions & 0 deletions docs/en/platform/knowledge/crawling.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ For a whole website, the crawler uses the homepage and published sitemaps, inclu

Scans are incremental. Unchanged content is skipped, changed content is indexed again, new pages are added, and removed pages leave the index — and so do pages `robots.txt` has come to disallow. A page that redirects to another address of the same site is indexed once, under the address it lands on. The row's page counts follow the scan as pages land, after discovery and after every stored batch, so the table moves while a scan runs. A URL list follows the same refresh schedule with its fixed selection. There is no separate publish step after successful indexing.

A scan asks for each page once. It sends along what the site's server said about the page at the last visit, and a server that answers `304 Not Modified` is not asked for the page again. Many servers build their pages on request and give no modification time; for those the crawler compares the text of the page it receives. Either way, a page that did not change is not opened in the browser, so none of its images, scripts and stylesheets are loaded. A page whose content is drawn by its own JavaScript is the exception: only a browser can read it, so it is rendered on every scan.

The crawler visits as an anonymous reader. Content that depends on a private session is not made accessible by adding its URL. It identifies itself on every request as `TaleBot/<version> (+https://docs.tale.dev/platform/knowledge/crawling)`, so a `robots.txt` group can address it by name — `User-agent: TaleBot` — to allow, throttle or refuse it alone.

The crawler applies `robots.txt` `Disallow` rules for the `*` agent on every path a URL can enter by — the sitemaps, the link walk and the links a rendered JavaScript page reveals — and again before every fetch: a page a rule covers is never fetched, and a page a rule added later covers leaves the index on the next scan. The rules do not filter an explicit URL list: a listed address is your instruction. On each content fetch, an HTTP `X-Robots-Tag: noindex` or `none`, or an HTML `<meta name="robots" content="noindex">` tag, prevents indexing, including for listed URLs, and drops whatever an earlier scan stored of that page. These rules are courtesy, not access control: do not rely on Tale's crawler as an access-control mechanism.
Expand Down
2 changes: 2 additions & 0 deletions docs/fr/platform/knowledge/crawling.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Pour un site entier, le crawler utilise l’accueil et les sitemaps publiés, y

Les scans sont incrémentaux : les contenus inchangés sont ignorés, les contenus modifiés réindexés, les nouvelles pages ajoutées et les pages retirées supprimées de l’index — comme les pages que le `robots.txt` en est venu à interdire. Une page qui redirige vers une autre adresse du même site n’est indexée qu’une fois, sous l’adresse où elle aboutit. Les compteurs de pages de la ligne suivent le scan à mesure que les pages arrivent, après la découverte et après chaque lot stocké, la table bouge donc pendant qu’un scan tourne. Une liste d’URL actualise sa sélection fixe au même rythme. Aucune publication séparée n’est nécessaire après l’indexation.

Un scan demande chaque page une fois. Il joint à sa requête ce que le serveur du site avait indiqué sur la page lors de la dernière visite, et un serveur qui répond `304 Not Modified` n’a pas à renvoyer la page. Beaucoup de serveurs construisent leurs pages à chaque requête et ne donnent aucune date de modification ; le crawler compare alors le texte de la page reçue. Dans les deux cas, une page inchangée n’est pas ouverte dans le navigateur, et ses images, scripts et feuilles de style ne sont donc pas chargés. L’exception est une page dont le contenu est construit par son propre JavaScript : seul un navigateur peut la lire, elle est donc rendue à chaque scan.

Le crawler visite en lecteur anonyme. Ajouter une URL ne rend pas accessible un contenu privé. À chaque requête il se présente comme `TaleBot/<version> (+https://docs.tale.dev/platform/knowledge/crawling)`, si bien qu’un groupe de `robots.txt` peut le désigner par son nom — `User-agent: TaleBot` — pour l’autoriser, le ralentir ou le refuser seul.

Le crawler applique les règles `Disallow` de `robots.txt` destinées à l’agent `*` sur chaque chemin par lequel une URL peut entrer — les sitemaps, le parcours de liens et les liens qu’une page JavaScript révèle après rendu — et de nouveau avant chaque récupération : une page qu’une règle couvre n’est jamais récupérée, et une page qu’une règle ajoutée plus tard couvre quitte l’index au scan suivant. Les règles ne filtrent pas une liste d’URL explicite : une adresse listée est ta consigne. À chaque récupération, un en-tête HTTP `X-Robots-Tag: noindex` ou `none`, ou une balise HTML `<meta name="robots" content="noindex">`, empêche l’indexation, y compris pour une URL de liste, et retire ce qu’un scan antérieur avait stocké de la page. Ces règles sont une courtoisie, pas un contrôle d’accès : si tu administres le site source, ne compte pas sur le crawler comme mécanisme de contrôle d’accès.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- migrate:up
--
-- What a scan checks a page by, before it spends a browser on it. Until
-- here the only way the crawler could tell whether a page had changed was
-- to do everything again: fetch it, render it in a browser with every image,
-- script and stylesheet the render loads, and compare content hashes
-- afterwards. A 700-page site scanned every six hours answered some 8,000
-- requests and two gigabytes a scan for pages that had not changed in
-- months (2026-10-06).
--
-- A scan now asks for each page once, and opens the browser only for a page
-- that did change. The row keeps what that one request is judged by:
--
-- etag the `ETag` of the response the stored text came from
-- last_modified its `Last-Modified`; the two are sent back as
-- `If-None-Match` / `If-Modified-Since`, and a server that
-- answers 304 is not asked for the page at all
-- probe_hash the hash of the text read out of the page's plain HTML,
-- for the servers that send neither: the same text again
-- is the same page
--
-- All three describe the response whose text the row stores AND has
-- indexed: they are written when a page settles and cleared when its
-- content is purged. They stay NULL for a page whose plain HTML does not
-- carry what a browser shows — a page built by its JavaScript — because a
-- 304 on such a shell, or the same shell text, says nothing about the
-- content; that page is rendered on every scan, as every page was.
--
-- `etag` and `last_modified` are in the baseline and went unused when the
-- render lane arrived; they are named here too so a corpus that was
-- bootstrapped without them converges. ADD COLUMN IF NOT EXISTS is
-- metadata-only, idempotent and rolling-safe — the previous image neither
-- reads nor writes the columns.
--
-- What the two columns still hold was written before the render lane, for
-- every page alike: a value there may belong to a JavaScript shell, and
-- sent back it would have that page answered 304 and never rendered again.
-- It is cleared once, here. Applied again (a corpus without a migration
-- ledger re-applies every file) it only costs each page one full visit.

ALTER TABLE public_web.website_urls
ADD COLUMN IF NOT EXISTS etag TEXT,
ADD COLUMN IF NOT EXISTS last_modified TEXT,
ADD COLUMN IF NOT EXISTS probe_hash TEXT;

UPDATE public_web.website_urls
SET etag = NULL, last_modified = NULL
WHERE etag IS NOT NULL OR last_modified IS NOT NULL;

-- migrate:down
-- Deliberately empty, like the baseline: dropping the columns would only
-- send every scan back to rendering every page. Remove with an explicit,
-- reviewed migration instead.
Loading
Loading