Context channels — how Webbee remembers
The three channels the web-kernel uses to feed live state into every LLM turn — skeleton, ctx.cache, and the fact-ledger.
The essence
Webbee feeds live state into the LLM's context window through three coordinated channels: the skeleton (kernel-refreshed awareness probe), ctx.cache (per-user TTL-bounded key-value store), and the fact-ledger (kernel-populated verbatim recall of recent tool results). Each channel is shaped by what its consumer needs: the intent classifier needs ambient awareness plus verbatim recent facts under a tight prompt budget; panels need rich page-level snapshots without bloating the classifier; handlers need ephemeral state shared within a session.
Your extension writes the skeleton (via @ext.skeleton) and ctx.cache (via ctx.cache.set()); the web-kernel writes the fact-ledger automatically after every successful tool call. The classifier reads all three at once, on every chat turn, before deciding what to do with the user's message. The narrator reads the result of any tool calls and renders the reply. The user sees only the chat output — but everything they see is grounded in those three channels.
Read this page when: you have data to surface to the LLM and you are not sure which channel to use; you are debugging "the LLM doesn't see X"; you are reviewing an extension pull request that touches more than one data surface.
Context channels vs data surfaces
A context channel is something the LLM sees as input on every turn (skeleton, fact-ledger, and ctx.cache when read by the classifier). A data surface is something your extension reads and writes directly (ctx.cache, ctx.store). ctx.cache belongs to both lists. ctx.store is only a data surface — its contents never reach the LLM unless your handler explicitly returns them.
The three channels at a glance
| Skeleton | ctx.cache | Fact-ledger | |
|---|---|---|---|
| What it is | Per-user per-section data probe | Per-user TTL'd key-value store | Verbatim JSON of recent ActionResult.data |
| Decorator / API | @ext.skeleton | ctx.cache.set() / .get() | None — kernel-populated automatically |
| Who writes | Extension handler | Extension handler | Web-kernel (after every successful tool) |
| Who reads | Intent classifier | Extension handlers, panels | Intent classifier |
| When refreshed | Every ttl seconds, per user | On demand | After every successful @chat.function |
| Lifetime | Until user uninstalls | Up to 300 s TTL | Last 5 turns |
| Visible to classifier? | Yes — every turn | No (unless explicitly read by a handler) | Yes — every turn |
| Max useful payload | ~2 KB (kernel compresses) | 64 KB per key | 3 KB aggregate per turn |
| Federal invariants | I-SKELETON-LLM-ONLY, I-SKELETON-STALENESS-ENVELOPE, I-SKEL-AUTO-DERIVE-1 | I-CACHE-VALUE-SIZE-CAP-64KB, I-CACHE-TTL-CAP-300S, I-CACHE-KEY-SAFETY | I-DISPATCH-RESULT-DATA-PLUMBED, I-FACT-LEDGER-PER-TURN-AGG-CAP, I-CROSS-TURN-FACT-LEDGER-DEEP-SERIALIZED |
Channel 1 — Skeleton (ambient awareness probe)
A skeleton is a small data probe your extension registers via @ext.skeleton. The web-kernel queries it on a per-user TTL schedule and injects every stored section into the intent classifier's prompt on every chat turn. The kernel compresses each section before injection — long strings truncate, lists of more than five dicts collapse to shape hints, only the first six fields of each section survive. You aim for ≤2 KB per section, knowing the classifier will see a compressed view bounded by those rules.
Use a skeleton for counts ("you have 8 unread emails"), flags ("two accounts connected"), and short recent-item lists (≤5 items the LLM can reference by name without a tool call). The classifier then routes precisely without making a tool call for trivia.
→ Deep dive: Skeletons
Channel 2 — ctx.cache (page-bound rich snapshot)
ctx.cache is a per-user TTL'd key-value store extensions own and operate directly. Unlike skeletons, it is not classifier-visible by default — its contents only reach the LLM if a @chat.function reads from it and returns the result. Use cases:
- A panel handler fetches 25 inbox messages from an upstream API once and caches them for 90 seconds; a chat function later reads from the cache instead of re-paying the upstream cost.
- An extension precomputes expensive aggregations during a skeleton refresh and stores them in cache for chat functions to surface on demand.
- Two handlers in the same session share intermediate state without round-tripping through storage.
Max 64 KB per key (I-CACHE-VALUE-SIZE-CAP-64KB), TTL bounded to 300 s (I-CACHE-TTL-CAP-300S), keys must match [A-Za-z0-9_\-:]+ ≤128 chars (I-CACHE-KEY-SAFETY).
→ Deep dive: Cache vs Store
Channel 3 — Fact-ledger (verbatim cross-turn recall)
The fact-ledger stores the exact JSON-serialised ActionResult.data returned by every successful tool call and replays the last five turns of those facts into the classifier prompt on every subsequent turn. There is no extension API — the kernel's session-memory producer writes the ledger after every successful dispatch. Your contract is to return clean structured data in ActionResult.data and the rest happens automatically.
The fact-ledger is the channel that makes anaphora resolution work: "send to the same address" finds the email in the prior turn's verbatim FACTS: line; "show that file again" finds the file ID; "delete the one I just created" finds the resource ID. Without the fact-ledger, the classifier would have only the narrator's prose summaries and the LLM would fabricate IDs (which historically it did — see the 2026-05-15 incident in the fact-ledger page).
→ Deep dive: Fact-ledger
Decision matrix — pick a channel
| If you want the LLM to… | Use this channel | Do not use | Why |
|---|---|---|---|
| Always know the user has 8 unread emails | Skeleton | ctx.cache (classifier doesn't read it) · fact-ledger (only populated after a tool call) | Counts and flags belong in the always-on awareness layer |
| Reference the 25 most recent inbox messages in a panel | ctx.cache | Skeleton (kernel collapses lists >5) · fact-ledger (bounded to last 5 turns + 3 KB aggregate) | Rich page-level data is ctx.cache's job |
| Recall the verbatim email address of a contact mentioned 3 turns ago | Fact-ledger (automatic) | Skeleton (it was never there) · ctx.cache (classifier doesn't read it) | Anaphora resolution is what fact-ledger exists for |
| Remember a user's saved preferences forever | ctx.store (cache-vs-store) | All three context channels — they are not persistent | Persistent state is a data-surface concern, not a context channel |
| Surface a one-line ambient alert when something changes | Skeleton with alert=True + paired @ext.tool | The other channels | Alert-on-change is built into the skeleton refresh loop |
| Render a list of 50 items the user can click on | @ext.panel with ctx.cache for the list payload | Skeleton (not for UI) · fact-ledger (not for UI) | Panels are the UI surface — see Panels |
| Give the LLM the last result of a tool the user just invoked | Fact-ledger (automatic) | Skeleton (TTL-bounded, not turn-bounded) | The kernel already plumbs this for you |
What each layer sees — a sequence
Three observers look at the same chat turn at three different moments. They see different things.
(1) Pre-turn — the intent classifier sees
Just before the classifier decides what to do with the user's message, the kernel constructs a prompt containing every stored skeleton section for the user, the user's recent turn history with FACTS: lines underneath each turn, the tool catalog, and the user's message. A fragment:
[SKELETON]
NOTE: every section below is a cached per-user snapshot. (...staleness header...)
- mail_inbox_summary (cached ~12s ago): accounts_connected=2, unread_total=8,
per_account=[sarah@work.com (#1), me@personal.com (#2)]
- tasks (cached ~28s ago): overdue_count=3, today_count=5, upcoming_7d_count=11
- notes (cached ~145s ago): total_notes=42, pinned_notes=4, recent_notes=[Q3 plan (#abc), Standup (#xyz), ...]
[HISTORY]
[2026-05-15T17:21:04Z user ok apps=[mail]] показать письма за сегодня → отправил 8 непрочитанных
FACTS: app=mail fn=list_inbox data={"unread": 8, "messages": [{"id": "abc", "from": "sarah@example.com", ...}]}
[2026-05-15T17:21:14Z user] отправь на тот же адрес "статус по проекту"The classifier never sees panel UINode trees, never sees full ctx.cache contents, never sees historical ActionResult.summary prose. It sees compressed skeleton sections + recent verbatim facts + tool catalog.
(2) During — your handler sees
When a @chat.function runs, it receives a Context object with the full set of surfaces your extension owns:
ctx.user— kernel-authoritative identity (imperal_id, role, language)ctx.store— persistent per-user statectx.cache— TTL'd key-valuectx.http,ctx.ai,ctx.notify,ctx.secrets— service clientsctx.skeleton.get(section)— only inside@ext.skeletonhandlers, raisesSkeletonAccessForbiddenelsewhere
Your handler does its work and returns an ActionResult with structured data, a summary for chat, and optional refresh_panels. The handler never sees the classifier prompt; it never sees other extensions' fact-ledger entries; it never sees the user's history (history is the classifier's concern).
(3) Post-turn — the narrator and the user see
After tools have run, the narrator (a second LLM call dedicated to producing chat prose) reads the ActionResult.summary plus the ActionResult.data for each step and produces the final reply the user sees in chat. Concurrently:
- The web-kernel writes a
ToolCallDigestentry into session memory for each successful tool — that becomes the fact-ledger entry for this turn. - Any
refresh_panels=[...]triggers panel re-fetches over SSE. - Audit ledger receives the action record (federal retention class).
The narrator never sees skeletons. The narrator never sees the classifier prompt. The narrator sees only what the tools returned this turn, plus the user's message — narrator's prose must remain grounded in that data per I-NARRATOR-NO-FABRICATED-CONTENT-CLAIMS.
What's next
Skeletons
Ambient awareness probe — counts, flags, short recent-item lists.
Fact-ledger
Verbatim cross-turn recall of recent [ActionResult](/en/reference/glossary/).data.
Cache vs Store
The two extension-owned data surfaces — TTL'd snapshots and persistent state.
Panels
The visual UI surface — rendered by React, not the LLM.
Federal invariants
The runtime contracts that keep all three channels coherent.