These are the knowledge base tools a Claude chat or Claude Code gets from osFoundry's MCP server. They were fetched with an unauthenticated tools/list on 2026-11-08 and trimmed to what matters here:
{"name": "search_knowledge_base",
"description": "Search current published articles in one granted knowledge base. Results cite the article ID and updated time; this is a live database read.",
"inputSchema": {"required": ["knowledge_base_id", "query"],
"properties": {"query": {"type": "string", "minLength": 2, "maxLength": 200},
"limit": {"type": "integer", "minimum": 1, "maximum": 20}}},
"annotations": {"readOnlyHint": true}}
{"name": "read_knowledge_article",
"description": "Read one current published article by ID from a granted knowledge base and return a stable citation.",
"inputSchema": {"required": ["knowledge_base_id", "article_id"]},
"annotations": {"readOnlyHint": true}}
{"name": "edit_knowledge_article",
"description": "Replace article content and optionally its title. Supply the version from read_knowledge_article; concurrent changes are rejected.",
"inputSchema": {"required": ["knowledge_base_id", "article_id", "content",
"expected_version", "idempotency_key"]},
"annotations": {"readOnlyHint": false, "destructiveHint": true, "openWorldHint": true}}
{"name": "poll_resource_events",
"description": "Resume from an event sequence for one granted channel, Note, knowledge base, record, or session.",
"inputSchema": {"required": ["resource_type", "resource_id"],
"properties": {"after_sequence": {"type": "integer", "minimum": 0},
"limit": {"type": "integer", "minimum": 1, "maximum": 100}}},
"annotations": {"readOnlyHint": true}}
Read the descriptions together and a design falls out. A search hit carries an article ID and an updated time. A read returns "a stable citation". An edit needs "the version from read_knowledge_article". And there's an event log per knowledge base you can resume from a sequence number. The updated time in the citation and the version an edit must name are, in practice, the same kind of value: osFoundry's own client for these tools, connected-cloud-tools.ts in the desktop app, types expected_version for edit_knowledge_article as format: date-time.
So a citation isn't only a pointer for the reader. It's a version stamp the agent can check later and, right after reading, hand back as a compare-and-swap token. Below is what that buys, measured on a rebuild.
What should a citation contain?
Three candidates:
| Citation | Survives a retitle | Shows the content changed | Usable as an edit version |
|---|---|---|---|
| Article title | no | no | no |
| Article ID | yes | no | no |
| Article ID + updated time | yes | yes | yes, if fresh |
The title is what a human reader wants to see, so it's tempting to cite by title alone and look it up again later. That only notices an article that was renamed or removed. An article whose text changed under the same title looks fine. The ID fixes renames. The time is what makes staleness checkable.
The rebuild
No Claude binary runs here. kb_store.py implements the four tools on sqlite with the live names, required arguments and limits (query 2–200 characters, limit up to 20 for search and up to 100 for events). Only published articles are searchable or readable. The JSON shapes are the rebuild's own, since the live server returns them only to an authenticated client. The citation is {knowledge_base_id, article_id, updated_at}. Every save writes an event {sequence, type, article_id, updated_at}, and edit_knowledge_article is a compare-and-swap on updated_at with an idempotency table, in one BEGIN IMMEDIATE transaction.
sim.py runs a simulated support day:
- 400 articles, 120 questions, 300 saves by people over 8 hours.
- Popularity follows a Zipf curve (s = 0.9). The top 10 articles get 36.7% of the questions and 36.7% of the saves, because the articles people ask about are the ones people keep fixing.
- Of the saves, 85% change content, 10% also retitle, 5% unpublish.
- For each question, a scripted "Claude" calls
search_knowledge_base, reads the top two hits withread_knowledge_articleand keeps both citations. - Every hour, it checks all citations given so far in three ways.
Seeds 1–5, Python 3.9.6 on an Apple M1, stdlib only. The whole run takes 25.7 s.
How fast do citations go stale?
Five seeds, 120 answers each, two citations per answer:
| Hour | Citations so far | Stale | Answers with a stale citation | Title-only check caught |
|---|---|---|---|---|
| 1 | 152 | 24 (15.8%) | 56.6% | 5 of 24 |
| 2 | 298 | 61 (20.5%) | 61.7% | 17 of 61 |
| 4 | 580 | 165 (28.4%) | 72.1% | 56 of 165 |
| 6 | 914 | 250 (27.4%) | 69.4% | 84 of 250 |
| 8 | 1,200 | 369 (30.8%) | 80.2% | 152 of 369 |
By the end of the day almost a third of all citations point at an article that has changed since Claude read it, and four answers in five have at least one such citation. "Changed" isn't "wrong": an edit may touch a paragraph the answer never used. But nobody can tell which without looking, and the time in the citation is what tells Claude where to look.
Popularity drives most of it. The control run spreads the same 300 saves uniformly over all 400 articles: 13.9% of citations go stale (167 of 1,200) and 46.7% of answers (280 of 600). The more your knowledge base is used, the more the articles Claude cites are the ones being edited.
The title-only check found 152 of 369 stale citations, 41.2%. Those were all retitles or unpublishes. It never sees a content edit.
Re-read every cited article, or poll the log?
Cost of the hourly checks over the day, summed over the five seeds (40 checks):
| Strategy | Calls | Data returned | Per check |
|---|---|---|---|
| Re-search each cited title | 2,426 | 3,342 KiB | 60.6 calls, 83.5 KiB |
| Re-read each cited article | 2,264 | 3,445 KiB | 56.6 calls, 86.1 KiB |
poll_resource_events from a cursor |
40 | 153 KiB | 1.0 call, 3.8 KiB |
Both ID-based strategies found exactly the same 369 stale citations as the ground truth. The script asserts that at every check. The difference is cost. Re-reading scales with how many articles you've cited and pays for their full text every time. Polling scales with how many changes happened. Fewer than 100 changes an hour fit in one page, so each check was one call, and Claude re-reads only the articles that moved. The cursor is a single integer to keep between turns. Before the first poll, the agent needs a sequence to start from, either 0 or the last sequence it stored with its citations.
What happens when Claude edits with the version from a citation?
The second experiment uses the same day. After each answer, Claude edits the first article it cited, passing the citation's updated_at as expected_version, after a delay. Five seeds, 150 edits per delay:
| Delay after the read | Rejected, version moved | Rejected, unpublished | Retry after a fresh read |
|---|---|---|---|
| 20 s | 0 (0.0%) | 0 | — |
| 15 min | 30 (20.0%) | 2 | 30 of 30 saved |
| 1 h | 57 (38.0%) | 3 | 57 of 57 saved |
| 4 h | 85 (56.7%) | 14 | 85 of 85 saved |
A citation is a good edit version for seconds and a poor one after an hour. The rejections are the system working: each of those writes would otherwise have replaced a person's newer text with content built from an older read. The fix is mechanical. Read again, re-apply the change to what's there, write with the new version. Every retry succeeded.
The idempotency key was checked too. After each successful edit, the same call with the same key was sent again. 409 of 409 returned the first result marked as a replay and wrote no new event.
Why does osFoundry answer only from published articles?
Because a citation should point at something a person decided is an answer. In the desktop app, an article's Publishing tab says "Published articles are used by this workspace's AI assistant and search. They are not visible outside the workspace." Drafts are invisible to search_knowledge_base and read_knowledge_article, and an article that's unpublished drops out of both on the next call. In the rebuild, unpublishing writes an event like any other save. 19 of the 600 delayed edits above hit an article that had been unpublished, and the check reports such a citation as gone, not as changed.
Why is the edit tool marked open-world?
edit_knowledge_article is the only knowledge base tool with openWorldHint: true. Its effect doesn't stay in the conversation. The saved article is what every other assistant and search in the workspace answers from next. The annotations tell the client that this call changes shared state, and Claude Code asks before running it unless you've allowed it. osFoundry adds its own gate when a linked chat works from the Knowledge Base screen inside the app: connected-browser-run.ts asks for approval on every cloud write and doesn't offer a session-wide grant for them.
If you wire this up yourself
- Put a version in the citation, not only an ID. Use the same value your edit tool takes as
expected_version, so one read gives you both. - Keep an event log per knowledge base with a monotonic sequence, and let the agent resume from a number. One integer of client state replaces re-reading everything it ever cited.
- Answer only from published content, and log unpublishing as an event, so a citation can come back as "gone" and not just as "different".
- Make edits whole-content compare-and-swap with a required idempotency key. Tell the model in the tool description to read again before editing, because a version from an hour-old citation fails about four times in ten.
- Mark the write tool destructive and open-world, and keep a per-call approval for it on your side too.
What osFoundry means by retrieval and grounding is in its RAG glossary entry. How its knowledge bases are organised is on the knowledge bases page.
Top comments (0)