Every fact in a chat answer can be traced back to NetSuite via 7 lineage stages: SOR → sync → D1 mirror → derived → Vectorize → tool exec + withSources wrap → chat answer. Each stage records its own watermark. _meta.as_of on every tool return surfaces the freshest contributing watermark, so the user sees how fresh the answer is. Stale answers trigger a staleness banner.
| Stage | Watermark source | "As of" field |
|---|---|---|
| 1 · NetSuite SOR | NS lastmodifieddate | live |
| 2 · Sync engine | sync_log.completed_at | ~5min lag (hot) / ~60min (cold) |
| 3 · D1 mirror | sync_log per table | last sync round |
| 4 · Derived data | derived.computed_at field | last compute |
| 5 · Vectorize | chunk.indexed_at | last index |
| 6 · Chat context | _meta.as_of (MAX over inputs) | freshest contributing watermark |
| 7 · Chat answer | rendered_at | when user saw it |
Every contributing read records a (table, last_synced_at) pair. When withSources() wraps a tool result, _meta.as_of = MAX(last_synced_at over contributing tables). If that timestamp is older than the user's tolerance window (or older than the freshness SLO for that table tier), the chat UI renders a yellow staleness banner before the answer.
This is the lineage guarantee: every fact in a chat answer can be traced to a source table + a watermark.
| Color | Meaning |
|---|---|
| frontend | User-facing surface (chat UI, admin HTML pages) |
| backend | Worker logic / agent code / business rules |
| database | D1 table / R2 object / KV key / Vectorize index |
| cloud | External system (NetSuite, Anthropic, etc.) |
| security | Gate / policy / HITL approval / kill switch |
| messagebus | Event ledger, Queues, async fan-out |
| external | Inbound source (email, webhook, cron tick, user input) |
| → solid | Synchronous call (request → response) |
| → green | Approved / happy-path |
| → red dashed | Policy or security check |
| → grey dashed | Optional / conditional / async |