Context capture
Last updated October 5, 2026
On this page
- How it works
- Sources
- Try it: add a source in the demo
- What is captured
- Who an item belongs to
- Curation
- The Curation page
- Signals
- Search: different people see different results
- Try it: search as two people
- Personal context and opting in
- Try it: opt Alice in and out
- Purge, retention and forget
- Forget a person
- The Distiller and Context threads
- Not possible yet
- For developers
- API
- CLI
The context module collects what your company already writes down in its own systems: meetings, email, documents, chat and issues. It files each item, links it to the customers, leads and contacts it is about, and makes it searchable. Nobody can read anything through it that they could not read at the source.
This page covers the context module as it works today on the golive branch.
By the end you will have added a source, opted a person in and out and seen
their items purged, searched as two different people (this one needs a model key), and
forgotten a person.
What is not possible yet is listed at the end.
The steps use the demo company from the Quickstart: run
ok demo and ok demo serve in the source tree, and sign in with the tokens
it prints. Without a model key the demo answers with the stub planner, a
stand-in that gives canned answers instead of a model (see
Without a model key). Capture,
linking, opt-in, purge and forget work the same with it; curation does not.
Each step that needs a real model says so.
How it works#
| Step | Who does it | What happens |
|---|---|---|
| Capture | The Context capture agent, every ten minutes, with no model | Reads each source whose schedule is due and keeps only what it is allowed to keep (see What is captured). |
| Link | Fixed rules, with no model | Ties each item to contacts and leads by the email addresses in it, and to organizations by exact name. |
| Curate | The Curator agent, with a model | Writes a summary, topics and a signal for each new item, ignores it, or leaves it for a person. Knowledge it proposes always waits for review. |
| Search | Everyone | A curated item can be found by everyone who may read it, and by nobody else. |
| Distill | The Distiller agent, once a week | Turns each team's decisions into proposed team knowledge and posts a weekly digest in the team's Context thread. |
The context module is installed and enabled in the demo company, but it has no sources. Until an admin adds one, nothing is captured.
Sources#
A source is one system the module reads from: a chat workspace, a mailbox, a
drive, a meeting recorder. A space is a place inside it where items are
shared, such as a chat channel or a shared folder. A source is reached
through a connection (see Connections and integrations)
whose server offers two
read-only operations: list items, and get one item. That can be a remote MCP
server, one of the built-in adapters (slack for channels, gmail for a
mailbox, gdrive for folders), or an adapter your company writes.
Admins manage sources on Modules: choose Open on the Context card, then the Sources tab. The table has the columns Source (with its kind, team and spaces), Connection (with the two tool names), Schedule, Policy, State (Active, Paused or Degraded), Last run, Items and Failed, and each row has Run now and Remove.
Add a source asks for:
| Field | What it does |
|---|---|
| Name | The source's id: lower case letters, digits and dashes, up to 32 characters, for example chat. It cannot change later. |
| Kind | meeting, email, document, chat, issue or other. |
| Connection | The connection that serves the two operations. |
| List tool, Get tool | The server's names for them. Default list_items and get_item. |
| Spaces | Comma separated. Shared items are captured from these spaces only, for example sales. |
| Policy | Shared only, or Shared, and personal by opt-in (see Personal context). |
| Schedule | How often it is captured, for example every:30m. |
| Team | The team shared items belong to, listed as for example Sales (team:sales). Its members can read them. |
| Keep for (days) | Retention. Empty means forever. |
| Back-fill (days) | How far back the first run, and each new opt-in, reaches. Default 30. |
When you choose Add, OrchKernel asks the connection for its tool list and refuses the source if the tools are not there, do not fit, or can write anything. The reasons are shown under the form, after "invalid:", for example:
- "server_tools: the server has no tool list_messages (for list_items)"
- "send_email: requires argument to, which the contract does not send", when you point a source at a tool that sends mail.
Keep capture and sending on separate connections. A connection that a source uses cannot be removed until the source is gone ("connection conn:team-chat is in use by ... context source chat").
Only human admins add, change or remove sources. Maya trying through the API gets "policy denied context.sources: context sources are managed by human admins".
Try it: add a source in the demo#
The demo has no chat or mail system to read, so you start a small local test
server that plays one. It comes with the build (cargo build --release builds
target/release/ok-mcp-fake too). Run the commands below in the source tree,
the folder that holds orchkernel-demo.
-
Make a folder
demo-source/sourcethere and save this asdemo-source/source/list_items.json:{ "server": "source", "tool": "list_items", "args": { "limit": 100 }, "result": { "items": [ { "id": "msg-1", "kind": "message", "title": "Litware wants SSO before renewal", "body": "Dana from Litware said on the call they need SSO before they renew in November. Alice will send the security overview.", "url": "https://chat.example/sales/msg-1", "author": { "id": "U-alice", "email": "alice@orchkernel-demo.example" }, "participants": [{ "id": "U-dana", "email": "dana@litware.example" }], "updated_at": "2026-10-04T10:00:00Z", "visibility": { "audience": "space", "space": "sales" } }, { "id": "msg-3", "kind": "message", "title": "Lunch on Friday", "body": "Who is in for SSO themed tacos on Friday?", "url": "https://chat.example/random/msg-3", "author": { "id": "U-eli", "email": "eli@orchkernel-demo.example" }, "participants": [], "updated_at": "2026-10-04T12:00:00Z", "visibility": { "audience": "space", "space": "random" } }, { "id": "msg-4", "kind": "message", "title": "Comp plan draft", "body": "SSO deals count double this quarter.", "url": "https://chat.example/leads/msg-4", "author": { "id": "U-sam", "email": "sam@orchkernel-demo.example" }, "participants": [], "updated_at": "2026-10-04T12:30:00Z", "visibility": { "audience": "restricted", "space": "sales" } }, { "id": "mail-5", "kind": "email", "title": "Re: Contoso pricing and SSO", "body": "Hi Alice, Jon here. Before we sign we need SSO and a 10 percent discount. Jon", "url": "https://mail.example/alice/mail-5", "author": { "id": "U-jon", "email": "jon@contoso.example" }, "participants": [{ "id": "U-alice", "email": "alice@orchkernel-demo.example" }], "updated_at": "2026-10-04T13:00:00Z", "visibility": { "audience": "personal", "space": "inbox", "owner": { "id": "U-alice", "email": "alice@orchkernel-demo.example" } } } ], "deleted": [], "next_cursor": null, "watermark": "2026-10-04T13:00:00Z" } }It holds four items: a message in the
saleschannel, one inrandom, one insalesshared with only a few people, and an email in Alice's own mailbox. -
Start the test server in its own terminal:
OK_MCP_FAKE_CONTRACTS=demo-source ./target/release/ok-mcp-fake --server source --http 127.0.0.1:8829It prints
listening on http://127.0.0.1:8829/mcp. Leave it running. -
Sign in as ada. Open Modules, the Connections tab, and choose Add a connection. Leave Adapter at "None: a remote MCP server", name it
team chat, set Endpoint (Streamable HTTP) tohttp://127.0.0.1:8829/mcpand choose Add: "Added team chat". Then press Test on its row: "team chat answered with 2 tools", and Last test shows Ok · 2 tools. -
Open Modules, choose Open on the Context card, then the Sources tab. It says "No sources yet." Choose Add a source and fill in: name
chat, kindchat, connection team chat, spacessales, policy Shared, and personal by opt-in, team Sales (team:sales). Leave the rest. Choose Add: "Added chat", and the row shows Active, last run never, 0 items.To see a refusal first, set List tool to
list_messagesand choose Add: "server_tools: the server has no tool list_messages (for list_items)". Set it back tolist_items. -
Press Run now. The note says "Ran chat" and the row shows 1 item and a last run time.
Only the sales message was kept. The random message is in a space the
source does not list, the comp plan is shared with only a few people, and
Alice's email is personal and she has not opted in.
Without Run now, capture runs on its own every ten minutes for each source whose schedule is due.
What is captured#
| The item is | Captured? |
|---|---|
| Shared with a whole space the source lists | Yes, for the source's team |
| Shared with a whole space the source does not list | No |
| Shared with only some people (a private channel, a document shared with a few) | Never |
| A person's own (their mailbox, their drive) | Only after that person opts in, and only for them |
| Deleted at the source | Purged on the next run |
| Purged earlier for any reason | Never again |
Each run reads at most 500 items or 20 MB per source and continues where it stopped next time. Quotes and signatures are stripped, very long bodies are cut, and an unchanged item writes nothing. Once a week, capture asks the source again about every item of the last 90 days and purges what has gone, stopped being shared, or moved out of the listed spaces.
Who an item belongs to#
- A shared item belongs to its source's team. Members of that team, admins and the context module's agents can read it. Nobody else can, not even Fiona the founder for a Sales item.
- A personal item belongs to its owner alone. Admins cannot read it either.
People are matched by their directory email first. When a system names people by its own ids (Slack names people by user id only), an admin gives each person their ids there through the API (see For developers). OrchKernel refuses an id that is someone else's directory email ("is the directory email of sam, not of tom") or already someone else's id ("U-alice already identifies alice"). People cannot set their own ids. Give ids with care: an id that points a person at someone else's account in the other system captures that someone's personal items for the wrong person once they opt in.
Curation#
The Curator gets new items in batches of up to 25, shared items and each
person's own items separately, at most 200 items a day per source (you can set
a different limit through the API). For each item it answers with a summary,
topics such as fn:sales, an optional signal (feature request, complaint,
praise, churn risk, competitor) and one or more actions, each with a
confidence from 0 to 100:
| Action | What happens |
|---|---|
| file | The summary, topics and signal are saved. The item is now searchable. |
| ignore | Set aside as noise, a duplicate, irrelevant or low confidence. Never searchable. |
| act | One task for a person. OrchKernel picks who, not the model (below). |
| extract | Proposed knowledge with the item as evidence. Always quarantined until someone accepts it on the Knowledge page (see Knowledge and decisions). |
| playbook_drift | A suggestion to the lead of an agent's team (shared items only). |
If any action is below 60, none of them runs and the item waits for a person on the Curation page.
Who gets a task. For a personal item, always its owner. For a shared item, the first of: a participant the model says committed to something, the owner of the record the item links to, the owner of a matching product area, the source team's lead, an admin. It is always an active person who can read the item, at most five such tasks a day each. The title and description come from a fixed template ("Follow up on a captured message ... nothing in this task was written by the model"), so text hidden in an email cannot write a task.
A model is needed. Captured content is restricted data, so the Curator
only uses a model cleared for it. Of the models OrchKernel sets up from an
environment key, Claude Sonnet, Opus and Fable (ANTHROPIC_API_KEY) and a
local Ollama model are cleared; the OpenAI and Gemini defaults and Claude
Haiku are not. See Models.
With the demo's stub planner (no key set), the Curator's run finishes as done but gets no usable answer: items stay new, nothing reaches the Curation page, nothing captured is searchable, and nothing on screen says so. Each later capture run hands the same items to the Curator again (on Work, under All, you see another "Curate 1 shared items from chat" task, done). Capture, linking, opt-in, purge and forget all work without a model.
The Curation page#
This section needs a model that can curate; with the stub planner the page only ever says "Nothing waits for you."
Curation in the sidebar lists the captured items left for you: the shared items of teams you lead (all of them for an admin) and your own personal items. Each shows the source, kind, "yours" for your own items or the team, when it was captured, why it waits ("The Curator was not sure enough to act"), what the Curator proposed, and a link to open it in the source.
- File opens a form: tick one or more functions (context, engineering, founder, marketing, other, product, sales, support) and optionally a signal, then File. The item becomes searchable.
- Ignore sets it aside.
With nothing waiting it says "Nothing waits for you." Admins also see Forgotten people here (see Forget a person).
After File the note says "Filed", but the item can stay in the list until the page refreshes.
Signals#
Signals, under Context in the sidebar, charts curated items by signal:
Items by signal, a weekly chart for each signal type (Feature requests by
week, Complaints by week, Praise by week, Churn risks by week,
Competitor mentions by week), Records signals concern most and Latest
signals. With nothing curated the signal charts read "No data" and Latest
signals reads "No rows.", but Records signals concern most already shows
a bar for each linked record, labeled with the start of its record id (for
example contacts:a41), because it counts every captured item with a link,
curated or not.
Search: different people see different results#
The search box at the top of the sidebar (on a phone, open the menu first) searches records and knowledge, curated captured items included, as you. Two people typing the same words see only what each may read.
Try it: search as two people#
This needs a model that can curate (see Curation); with the stub planner the captured item is never curated and never found.
-
As ada, press Run now on the
chatsource if you have not, and wait a minute for the Curator. -
Sign in as alice (Sales) and type
SSOin the search box. You see Litware wants SSO before renewal, kind Context items, with the start of the message. Clicking it opens the item's fields: its summary, topics, signal and the ids of the records it links to (the contact Dana Brooks and the lead Litware). -
Sign in as maya (Support) and type
SSO. You see "Nothing you may read matches." -
In a terminal, the CLI gives the same answers:
ok search SSO --state orchkernel-demo/state.db --as alice # the Litware item ok search SSO --state orchkernel-demo/state.db --as maya # prints "no results"
Under the hits you may read "Matched on your words only: search by meaning is not set up here." That means no embedding model is set up; keyword search still works.
Items that are new, waiting for a person, or ignored are never hits. An item the source changes leaves search until it is curated again.
Personal context and opting in#
On a source whose policy is Shared, and personal by opt-in, each person can let the module capture their own items (their mail, their documents) for them alone. Nothing personal is captured until they opt in.
Open your Profile (your name at the bottom of the sidebar; on a phone, open the menu first). Under Personal context each source that allows it shows "chat · not opted in", Off and Opt in, or "chat · opted in" with the time, On and Opt out. "No source allows personal opt-in." means there is none.
| Opt in | Opt out | |
|---|---|---|
| Who | Only the person, signed in as themselves. An agent acting for them cannot. | The person, or an admin for them (API or CLI). |
| What it does | Gives the Curator a narrow permission for that source only: no tools, captured items only. The next capture reaches back Back-fill (days) for the person's own items. | Revokes that permission, cancels their curation runs for the source, and purges their items from it together with what was made from them. |
| Shared-only source | Refused: "source support-chat captures shared spaces only" | Not applicable |
Opting in creates a delegation from you to the Curator (see Delegations). Revoking that delegation directly ends the opt-in the same way as opting out, but only at the source's next capture run; until then Profile still shows it On.
Try it: opt Alice in and out#
- Sign in as alice, open Profile. Under Personal context the
chatsource says "not opted in" and Off. Choose Opt in: "Opted in to chat", and the row turns On. - As ada, press Run now on the
chatsource. The row now shows 2 items. - The email Re: Contoso pricing and SSO is now captured for Alice alone.
With a model, the Curator files it or leaves it on her Curation page;
once filed, searching
Contosoas alice finds it, and the same search as ada does not: an admin cannot read Alice's own items. With the stub planner the email stays new, so searchingContosofinds only the Contoso Health lead. - As alice, on Profile, choose Opt out. A confirmation says "Opt out of chat? What was captured from your own items there is purged." Choose Opt out in it: "Opted out of chat", and the row is Off again.
- As ada, the
chatrow is back to 1 item. With a model, searchContosoagain as alice: the email is gone and only the Contoso Health lead remains.
Opting in again later does not bring the email back: a purged item is never captured again.
Purge, retention and forget#
A purge is for good. It removes the item and every version of it, its search entries, any private or quarantined knowledge citing it, the text of it held in waiting plans and approvals, and, for a personal item, the tasks made from it (cancelled, their words replaced with "removed"). Team knowledge a person already accepted stays, and its evidence reads "source removed". A task made from a shared item stays with its assignee, still pointing at the purged item.
| What purges | Which items |
|---|---|
| The source deletes or stops sharing an item | That item, on the next run or the weekly recheck |
| Retention (Keep for (days)) | Items captured longer ago than that, in any state, checked on every capture run, even for a paused or degraded source |
| Opting out | The person's own items from that source |
| Remove on a source | Everything captured from it, after the confirmation "Remove chat? Everything captured from it is purged." |
| Forget a person | Their own items and private knowledge; their shared items only if an admin chooses Purge |
A removed source's items are not captured again even if you add a source of the same name. To capture them again, use another name.
Backups keep purged content until they age out (see Self-hosting, backups and upgrades).
Forget a person#
When someone leaves, or asks to be forgotten, an admin runs forget. It purges their personal items and private knowledge, revokes their opt-ins, and flags the shared items they wrote or took part in for the admin to decide on. There is no button for it yet; use the CLI or the API.
-
In a terminal, as ada:
ok context forget --person alice --state orchkernel-demo/state.db --as adaIt prints what it did, for example
{ "person": "alice", "optins": 0, "items": 0, "knowledge": 0, "flagged": 1 }. Hereoptinscounts the opt-ins revoked (0 here, since Alice opted out above; 1 if she were still opted in),itemsandknowledgewhat was purged, andflaggedthe shared items left for you, here the Litware message Alice wrote. Run as anyone else, for example--as maya, it is refused: "policy denied context.forget: forgetting a person is for human admins". -
As ada, open Curation. Under Forgotten people each flagged item shows its title and "chat · message · forgot Alice Chen · flagged by Ada Park", with Purge and Keep.
-
Choose Keep to leave the item for the team ("Kept"), or Purge, then Purge again in the confirmation "Purge this item and what was derived from it? This cannot be undone." ("Purged"). Either way the flag goes and your choice is recorded.
Forgetting the same person again flags the same shared items again, even the ones you chose Keep for.
Another person's own items are never flagged, since the list would show them to the admin.
The Distiller and Context threads#
Each team with a source (or bound to the context module) that has a lead gets
one Context thread, from the Team context template. The lead owns it;
the team's members, its agents and the Distiller take part. In the demo, after
you add the chat source, Sam (and Alice, as a member) finds Context:
team:sales under Your threads, marked Team context. The thread's goal
shows the team's id (team:sales), not its name. If the lead changes, the thread passes to the new lead and the old
lead stays in it as a contributor while still on the team.
Once a week the Distiller reads the team's week (only what every member can see) and proposes, always quarantined and owned by the team:
- a decision entry for each decision logged in the team's threads;
- framework, enablement and SOP entries from Context thread artifacts named
that way (
framework-...,enablement-...,sop-...); - an insight and a suggestion to the lead for each reason given twice for rejecting an agent's work.
It then posts a digest artifact in the Context thread, named for the day and
time, such as digest-2026-10-05-114855. It starts "# The week of
team:sales to 2026-10-05" and counts runs, approvals, thread decisions,
entries proposed for review and playbook suggestions to the lead, ending
"Everything proposed waits for a reviewer in the knowledge library." Its
posts mention nobody and wake nobody, and you cannot ask the Distiller in
the thread: the agent picker lists it as "Distiller (you can't ask it: it
acts for people who delegated to it)". The first digest is posted
right after the team's first capture, then weekly. See Threads
for working in the thread.
Not possible yet#
- Forgetting a person from the screen (CLI and API only).
- Editing a source from the screen. Remove it and add it again, or use the API. Changing its connection, spaces or policy starts it over from its back-fill.
- Pausing a source from the screen (API only).
- Seeing an item's links on their own, or a list of all captured items, from the Curation page. Search and the API show them; the item view lists links as record ids, not names.
- Setting people's ids in other systems from the screen (API only).
- Running the Distiller on demand.
- Curation in the demo without a model key.
For developers#
API#
All under /api with Authorization: Bearer <token>.
| Method and path | Who | Notes |
|---|---|---|
GET /context/sources, GET /context/sources/:id |
Admin | Each source with state, last_run, items, failed, cursor. |
POST /context/sources |
Admin | { name, kind, connection, server_tools: { list, get }, spaces, policy, schedule, team, retention_days?, backfill_days?, curation_cap? }. 422 invalid with problems when the tools do not fit. |
PUT /context/sources/:id |
Admin | Change fields, including state: "paused". |
DELETE /context/sources/:id |
Admin | Revokes its opt-ins and purges its items. |
POST /context/sources/:id/run |
Admin | { recheck? }. Returns { run, task, output }; output.sources[] has captured, unchanged, skipped, purged, failed, errors. |
GET /context/optins |
Anyone | { available, optins } for the caller. |
POST /context/optins, DELETE /context/optins/:source |
The person | { source }. An admin may add ?person=<id> to the delete. 409 optin_not_allowed on a shared-only source. |
GET /context/identities/:actor, PUT /context/identities/:actor |
Admin, or the person reading their own | Send { "external_ids": { "slack": ["U012AB3CDE"] } }, keyed by a source's name or kind; the answer is the map itself. A person setting their own gets "external ids are set by human admins". |
GET /context/inbox, GET /context/functions |
Anyone | The caller's curation inbox; the functions and signals to file with. |
GET /context/items/:id/links |
Anyone who can read the item | Each link with its title, or "restricted record" for a record the caller may not read. 404 for an item the caller may not read. |
POST /context/forget |
Admin | { person }. |
GET /context/flags, POST /context/flags/:item |
Admin | { choice: "purge" or "keep", person? }. |
GET /search?q= |
Anyone | { hits, keyword_only } as the caller; truncated: true when a very common word was cut short. |
Filing and ignoring write state, topics and signal on the
context_items record through POST /brain/collections/context_items/records.
A person may change only state (from waiting), topics, signal and links; the
Curator and the capture agent each have their own fields; anything else is
403.
A source's server must answer list_items({ since, cursor, limit, spaces })
with { items, deleted, next_cursor, watermark } and get_item({ id }) with an
item or null. The schemas are in crates/ok-mcp/contracts/source/.
CLI#
ok context sources # admin: every source and its state, as JSON
ok context run chat [--recheck] # admin: capture now
ok context optin chat # opt yourself in
ok context optout chat [--person alice] # opt out; an admin may name the person
ok context forget --person alice # admin
ok search "SSO before renewal" [--collections context_items] [--limit 20]
Each takes --state <path/to/state.db> and --as <person>, and works while
ok demo serve is running. The CLI works on the state file directly and does
not use the demo's local network allowance, so in the demo ok context run chat against the source on 127.0.0.1 captures nothing and lists "list_items
failed at the source" under errors; use Run now or the API there.
Design notes: docs/context.md in the source tree.