Skip to main content
Update your context layer incrementally as signals land and change. Rebuilding the store from scratch is wasteful, and re-reading by hand misses things. A cursor remembers exactly where you left off, so each refresh pulls only what’s changed into your store, in order, with nothing missed or double-counted.

The recipe

sort=changed is a change feed: signals ordered by when they last changed. You page it with a cursor, and the cursor is a bookmark you keep between runs. Poll hourly, daily, or after a week away, and you always pick up exactly where you left off.
1

Resume from your cursor

Pass your saved cursor to signals.list(sort="changed"). It returns everything that changed since that point. The first run has no cursor, so it does a full initial sync; every run after is just the delta.
2

Upsert by signal id

Replace each changed signal in your store by its signal_id: newest wins, wholesale. A signal’s entities can change over time as they’re merged or renamed, so you replace the whole record; merging would leave stale parts behind.
3

Save the cursor for next time

Persist the updated store and the new cursor. Poll on any cadence (hourly, daily, after a week away) and your territory stays current, so Search and the rest of your loop always read fresh, verified data.
Install gildea, set GILDEA_API_KEY, then (pure SDK, no model calls):
import json
import pathlib
from gildea import Gildea

gildea = Gildea()
CURSOR = pathlib.Path("update_cursor.txt")    # remembers exactly where the last sync ended
STORE = pathlib.Path("signals.jsonl")         # your local copy of the signals you track

# 1. Resume from your saved cursor (none on the first run -> a full initial sync).
cursor = None
if CURSOR.exists():
    cursor = CURSOR.read_text().strip() or None

# 2. Page the change feed: everything changed since last sync, in document order.
#    The first run has no cursor, so it backfills; we cap the initial pull to keep the demo
#    quick (raise MAX_PAGES, or drop the cap, to backfill the full history). Steady-state runs
#    resume from the saved cursor and only pull the small delta. Pull the whole feed (not
#    filtered by entity) so you also catch signals that *lost* an entity (merge/rename); route
#    to your tracked scope client-side from each signal's entities.
changed, MAX_PAGES = [], 5
for _ in range(MAX_PAGES):
    page = gildea.signals.list(sort="changed", cursor=cursor, limit=50)
    changed.extend(page["signals"])
    cursor = page.get("next_cursor") or cursor        # advance; keep old if nothing newer
    if not page["has_more"]:
        break

# 3. Upsert by signal_id: newest version wins, replace the whole record (don't merge: a
#    signal's entities can change (merge/rename), so stale parts must drop out).
store = {}
if STORE.exists():
    store = {s["signal_id"]: s for s in map(json.loads, STORE.read_text().splitlines()) }
for s in changed:
    store[s["signal_id"]] = s

# 4. Persist the store and the cursor for next time.
STORE.write_text("\n".join(json.dumps(s) for s in store.values()))
CURSOR.write_text(cursor or "")

print(f"{len(changed)} signals changed this sync; {len(store)} tracked total")

What you get

A self-healing watch on your scope: poll on any cadence (hourly, daily, after a week off) and you always get exactly what changed since last time, with nothing missed or repeated. The cursor makes it incremental; the upsert keeps every record current. It writes to the same signals.jsonl your other recipes read, so your context layer stays current without a full rebuild.

Search for signals

Embed signals