Imperal Docs
Core Concepts

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

Skeletonctx.cacheFact-ledger
What it isPer-user per-section data probePer-user TTL'd key-value storeVerbatim JSON of recent ActionResult.data
Decorator / API@ext.skeletonctx.cache.set() / .get()None — kernel-populated automatically
Who writesExtension handlerExtension handlerWeb-kernel (after every successful tool)
Who readsIntent classifierExtension handlers, panelsIntent classifier
When refreshedEvery ttl seconds, per userOn demandAfter every successful @chat.function
LifetimeUntil user uninstallsUp to 300 s TTLLast 5 turns
Visible to classifier?Yes — every turnNo (unless explicitly read by a handler)Yes — every turn
Max useful payload~2 KB (kernel compresses)64 KB per key3 KB aggregate per turn
Federal invariantsI-SKELETON-LLM-ONLY, I-SKELETON-STALENESS-ENVELOPE, I-SKEL-AUTO-DERIVE-1I-CACHE-VALUE-SIZE-CAP-64KB, I-CACHE-TTL-CAP-300S, I-CACHE-KEY-SAFETYI-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 channelDo not useWhy
Always know the user has 8 unread emailsSkeletonctx.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 panelctx.cacheSkeleton (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 agoFact-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 foreverctx.store (cache-vs-store)All three context channels — they are not persistentPersistent state is a data-surface concern, not a context channel
Surface a one-line ambient alert when something changesSkeleton with alert=True + paired @ext.toolThe other channelsAlert-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 payloadSkeleton (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 invokedFact-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 state
  • ctx.cache — TTL'd key-value
  • ctx.http, ctx.ai, ctx.notify, ctx.secrets — service clients
  • ctx.skeleton.get(section)only inside @ext.skeleton handlers, raises SkeletonAccessForbidden elsewhere

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 ToolCallDigest entry 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

On this page