docs: the wiki is written in docs/wiki and published on every push to main by publish-wiki, with invisible_playwright_mcp's converter; its first articles are the studies behind a Dot's memory, its token sizing, its compaction and its benchmarks

This commit is contained in:
feder-cr committed 2026-10-09 19:19:59 +02:00
1 parent ad4fbf3306
commit 3820581c6a
8 files changed
+633

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
# Publishes docs/wiki/ to the GitHub wiki of this repository on every push to main that changes it.
#
# The wiki is its own git repository (<repo>.wiki.git): flat pages, no front matter, a _Sidebar.md. The render is
# invisible_playwright_mcp's own (scripts/build_wiki.py and scripts/sync_article_pixels.py, MIT, fetched at a fixed
# commit rather than copied): it strips the front matter, rewrites [x](page.md) links to the wiki's form, names
# index.md Home, builds the sidebar from parent/nav_order, and gives every page its view pixel (a release asset of
# the `article-views` release, created on first use). The wiki checkout is emptied of pages before the copy, so a
# page deleted from docs/wiki/ leaves the wiki too.
name: publish-wiki
on:
push:
branches: [main]
paths:
- "docs/wiki/**"
- ".github/workflows/publish-wiki.yml"
workflow_dispatch: {}
permissions:
contents: write
concurrency:
group: publish-wiki
cancel-in-progress: true
env:
# invisible_playwright_mcp's main on 2026-10-09: the converter this wiki is rendered with.
CONVERTER_REPO: feder-cr/invisible_playwright_mcp
CONVERTER_REF: b72e8b1937771149ed06b2ba73ccb9cb32a687f8
jobs:
publish-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Fetch the converter
run: |
set -euo pipefail
mkdir -p wiki_tools
for script in build_wiki.py sync_article_pixels.py; do
curl -fsSL -o "wiki_tools/$script" \
"https://raw.githubusercontent.com/${CONVERTER_REPO}/${CONVERTER_REF}/scripts/$script"
done
- name: Render docs/wiki/ into wiki pages
run: python wiki_tools/build_wiki.py docs/wiki wiki_build --pixels "${{ github.repository }}"
# Upload before the push: a page published without its asset counts nothing until the next run.
- name: Give every new page its view pixel
env:
GITHUB_TOKEN: ${{ github.token }}
run: python wiki_tools/sync_article_pixels.py --repo "${{ github.repository }}" wiki_build
- name: Check every page carries its own pixel
run: |
python wiki_tools/sync_article_pixels.py --selftest
python wiki_tools/sync_article_pixels.py --repo "${{ github.repository }}" --check wiki_build
- name: Publish to the wiki
env:
WIKI_PUSH_TOKEN: ${{ secrets.WIKI_TOKEN != '' && secrets.WIKI_TOKEN || github.token }}
run: |
set -euo pipefail
repo="${{ github.repository }}"
git clone "https://x-access-token:${WIKI_PUSH_TOKEN}@github.com/${repo}.wiki.git" wiki
find wiki -maxdepth 1 -name '*.md' -delete
cp wiki_build/*.md wiki/
cd wiki
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "wiki already up to date"
else
git commit -m "docs: sync wiki from docs/wiki (${{ github.ref_name }})"
git push origin HEAD:master
fi
+133
View File
@@ -0,0 +1,133 @@
---
title: "Agent memory systems, measured on the same questions"
description: "Mastra Observational Memory, LangMem, Hindsight, an agentic dream pass and a single request, run on the same LongMemEval questions on real agents, with accuracy and cost per pass. Plus what changes with years of history."
parent: "Studies"
nav_order: 2
---
# Agent memory systems, measured on the same questions
A Dot keeps a profile of the person, `MEMORY.md`, in every prompt (see
[How a Dot remembers](how-a-dot-remembers.md)). Something has to write it from
the conversations. Many open-source projects do this; most publish their own
scores on their own setups, and the setups differ. So the candidates were run
here on the same questions, the same agent and the same model.
## The setup
- **Questions:** the 30 LongMemEval questions whose answer depends on what the
person likes ("can you suggest a hotel for my trip?"), each with about 48
dated chats, 115,000 tokens, as its history. These are the ones a profile is
for; the other kinds were checked separately on 116 mixed questions.
- **Agent:** a new Dot per question, with the history as its conversation
files. Each method writes `MEMORY.md` from that history; then the question
goes to the Dot as a task. The Dot can also grep the files, so every method
is measured on top of the same baseline.
- **Model:** z-ai/glm-5.3-flash for everything, through OpenRouter; GPT-4o
grades with LongMemEval's own prompt.
- **Runs:** two per method where the budget allowed. Two identical runs with no
profile scored 19 and 20 out of 30, and others moved by up to three
questions, so a single run of 30 cannot tell methods apart; a difference of
two questions out of 60 is noise.
- **Faithfulness:** each project's own prompts, copied verbatim (18 of 18
checked against the sources), its own call pattern, and its own benchmark
settings where it publishes them. Every adaptation is marked in the adapters,
which are kept in commit `de3d9a1b` of the repository.
## Results
| Method | Correct | % | Cost of a pass over 115K tokens | Calls | Needs |
|---|---|---|---|---|---|
| No profile (the Dot greps the files) | 39/60 | 65% | 0 | 0 | nothing |
| **One request (ours)** | **50/60** | **83%** | **~$0.015** | **1** | nothing |
| Hindsight (MIT) | 25/30 | 83% | ~$0.25 | ~255 | Postgres + pgvector, ~1.9 GB RAM |
| Mastra Observational Memory (Apache-2.0) | 48/60 | 80% | ~$0.05 | ~6 | (TypeScript) |
| LangMem profile manager (MIT) | 48/60 | 80% | ~$0.02 | 1 | LangChain |
| Agentic pass with file tools (nanobot's Dream, MIT) | 44/59 | 75% | ~$0.08 | ~20 steps | nothing |
| Memobase (Apache-2.0) | stopped | | ~$0.25-0.40 | ~200 | Postgres + Redis |
| Mem0 (Apache-2.0) | not run | | ~$0.40 | ~250 | a vector store |
| Letta sleep-time agents (Apache-2.0) | not run | | ~$1.10-1.60 | ~120, growing | a server + Postgres |
The costs of the last three are estimated from their own call patterns,
measured against a stand-in model: running them through would have cost more
than the rest of the study together, for designs the measured ones already
covered (Letta's is an agentic pass like Dream's, which was measured).
## What the numbers say
**The profile matters more than the method.** Every method that writes one
moves the score from 65% to about 80%. Between the one request, Hindsight,
Mastra and LangMem the differences are two questions out of 60 or less:
inside the noise.
**At equal quality, cost decides, and it varies seventeenfold.** The single
request reads the conversations once and answers with the whole new profile.
Mastra reads them in 30,000-token chunks and rewrites its notes when they grow.
Hindsight extracts facts from every ~1,800 characters, one call each. The
agentic pass opens files one at a time with tools, and every step sends the
whole growing context again: 115,000 tokens of history become about 500,000
tokens paid. Letta's sleep-time agent does the same while keeping its own
transcript, which is why its estimate is the highest.
**Simpler is also more robust.** The single request has no tools to call and
no state of its own; the agentic pass failed in 13 of 30 runs before a token
sizing bug was fixed (see
[Token counts are not portable between models](token-counts-across-models.md)),
and Memobase's run ended when the credit did.
**On the 116 mixed questions the profile helped where expected and hurt
nowhere:** updates from 78% to 94% (the profile keeps the newest version of a
fact, with its date), preferences from 57% to 71%, the other kinds within one
question.
## What was chosen
The single request, as a background pass: once the Dot has been quiet for
five minutes, the conversation files that changed go to the summary model with
the current `MEMORY.md`, and the answer is the new `MEMORY.md`. From Mastra it
takes two ideas rather than code: reading in pieces when the new conversations
do not fit one request, and saying that a changed fact replaces the old one.
From LangMem, waiting for a quiet spell rather than running after every
message.
## With years of history (reasoning, not measured)
A Dot used every day for a year holds on the order of ten million tokens of
conversation. What changes:
- **The files and grep hold.** Searching 40 MB of text is instant, and nothing
is lost, because the files are the words as said. What degrades is
precision: a common word matches hundreds of times. The first remedy costs no
model call: a ranked keyword index (BM25) inside the Dot's computer.
- **One profile stops being enough.** A year of a working life does not fit in
25,000 characters. The pass then needs two levels: `MEMORY.md` as the core,
and notes by subject (clients, health, home) that it writes when the core is
full, each named by a line in the core. That is the part of Mastra's design
(its Reflector) worth taking later.
- **The cost of the pass stays flat**, because it reads only what is new:
around a cent a day, a few dollars a year. A monthly rebuild of the profile
from the files, about $2 at that size, would stop small errors from
compounding over hundreds of rewrites.
- **Hindsight gains, and so does its bill.** Retrieving facts for each question
from millions of tokens is its strength, and the gap to a bounded profile
would grow for specific details. Its cost grows with the history (about $22 a
year of extraction at this rate, plus a database per Dot), and its facts are
extractions, lossy where the files are verbatim. For specific details the
files already do that job.
- **The agentic designs and per-message extraction get worse**: the first
reread more every pass, the second pay on every message.
- **Contradictions are the real risk** ("I live in Milan", then months later "I
moved to Rome"). A rewrite with dated facts handles them; systems that only
accumulate facts keep both.
## Sources
- LongMemEval: [paper](https://arxiv.org/abs/2410.10813), [code](https://github.com/xiaowu0162/LongMemEval)
- Mastra Observational Memory: [research](https://mastra.ai/research/observational-memory), [code](https://github.com/mastra-ai/mastra)
- LangMem: [code](https://github.com/langchain-ai/langmem)
- Hindsight: [paper](https://arxiv.org/abs/2512.12818), [code](https://github.com/vectorize-io/hindsight), [benchmark harness](https://github.com/vectorize-io/agent-memory-benchmark)
- Memobase: [code](https://github.com/memodb-io/memobase)
- Mem0: [paper](https://arxiv.org/abs/2504.19413), [code](https://github.com/mem0ai/mem0)
- Letta: [code](https://github.com/letta-ai/letta), [sleep-time compute](https://arxiv.org/abs/2504.13171)
- nanobot (Dream): [code](https://github.com/HKUDS/nanobot)
- Is Grep All You Need?: [arXiv 2605.15184](https://arxiv.org/html/2605.15184)
@@ -0,0 +1,92 @@
---
title: "Benchmarking an agent on real VMs"
description: "How Dots are tested as a person meets them: Harbor tasks in a new VM each, long builds of two to three hours, eight Dots on one host, and the defects these runs found."
parent: "Studies"
nav_order: 5
---
# Benchmarking an agent on real VMs
A Dot is an agent with its own computer: a QEMU virtual machine on the host,
reached through an API. Unit tests and a stand-in model check the engine's
logic; they cannot tell whether a Dot gets real work done. These runs do, on
real VMs and a real model, and every one of them found something.
## The harness
[Harbor](https://github.com/harbor-framework/harbor) (Apache-2.0), the harness
of Terminal-Bench, runs a task in an environment and grades what is left. Here
the environment is a Dot: a new one per trial, created through the product's
API, with the task's Dockerfile replayed inside it as the Dot's own user (there
is no root: `apt-get install` becomes the Dot's `sudo dot-install`), and the
task's paths moved under its home. The agent is the Dot itself, given the
instruction as a task. A task counts only once its reference solution scores 1
and doing nothing scores 0 **in a Dot**: a task that needs root fails its
oracle, which is how such a task is found. Code: `tests/bench/`.
## Short tasks
25 tasks written for this, in shell, files and documents, coding, data
analysis, the computer's settings, questions with one answer, two long
pipelines, and three about what a Dot is for (its own automation must run
twice; clean up leftovers and nothing else; say a request is impossible). The
data is generated with a fixed seed and the expected answers are computed by
the authoring script, never by an agent.
- Oracle 25/25, doing nothing 0/25.
- A Dot on z-ai/glm-5.3-flash: **73 of 75 trials** (each task three times).
Both misses were the same task, a Bash script to port to Python.
## Long tasks: two to three hours
Five tasks where the Dot builds or researches something whole, each graded by
checking the result itself, never by asking a model:
| Task | What the Dot does | What the grader checks |
|---|---|---|
| chess-perft | a chess move generator, every rule included | published perft counts of five positions to depth 4 and 5 (up to 4.9 million), and four positions it has not seen |
| lisp-interpreter | an interpreter for a Scheme subset | 30 programs it has not seen: closures, tail calls a million deep, recursion 5000 deep, errors |
| kv-store | a key-value server over HTTP | the API, TTLs, 1000 writes from 20 clients at once, every acknowledged write after `kill -9` |
| language-history | research on 18 programming languages, on the web | years and creators, influences, a cited 1500-word report from five sites |
| fraud-investigation | an investigation of 292,467 generated card transactions | exactly the planted cases of four kinds of fraud, a script that finds them again |
These found the token limits described in
[Compacting a thread at the model's own window](compacting-at-the-models-window.md):
with a 4,096-token answer cap, a Dot writing a whole program in one call looped
until it failed. After the fix all five passed, the server and the interpreter
two out of two, in runs of 18 to 75 minutes.
## Memory: LongMemEval
The same harness runs [LongMemEval](https://github.com/xiaowu0162/LongMemEval)
on Dots: a history of 48 chats becomes a Dot's past conversations, the question
is a task, GPT-4o grades outside the Dot. The results and what they changed are
in [How a Dot remembers](how-a-dot-remembers.md) and
[Agent memory systems, measured on the same questions](agent-memory-systems-compared.md).
This run found two defects of its own: a token estimate OpenRouter refuses
(see [Token counts are not portable between models](token-counts-across-models.md)),
and in the harness a call to a Dot that could wait forever, which once held
four questions for seven hours; every call now has a bound.
## Many Dots on one host
`tests/e2e/scale.ts` starts 1, 2, 4 and 8 Dots at once on one host; each must
come up, answer a message and do a task, within budgets that are ratios to one
Dot. All four steps passed. Measured: the time to READY at the 95th percentile
went from 21 s with one Dot to 45 s with eight; the API answered in 6 to 10 ms
at the 95th percentile throughout; each Dot's QEMU took about 700 MB.
## What the other tests found
- **Approvals**: every tool under allow, deny, ask-then-approve and
ask-then-reject, in the chat and in a task (258 cases), plus a race of an
approve and a reject sent at the same moment, 50 times on PostgreSQL: one
wins, the other is refused, the Dot hears one decision. It found that a task
cancelled while a call waited for approval left the approval pending forever,
so the Dot never went back to READY and never slept.
- **Upgrades**: the databases a Dot and a host of a released version left
behind open under the new build and keep working.
Every run here costs real money on a real model; the numbers above were bought
for a few dollars each, and the harness keeps each Dot's events, engine log and
memory with its result so a wrong answer can be read after the Dot is gone.
@@ -0,0 +1,81 @@
---
title: "Compacting a thread at the model's own window"
description: "Why a Dot sets no token limit of its own, when it compacts a long thread, and why it clears old tool results before it summarizes anything: the rules taken from opencode, Codex, OpenHands and a SWE-bench study."
parent: "Studies"
nav_order: 4
---
# Compacting a thread at the model's own window
An agent working for hours fills its context. Something has to give, and
every choice costs something: a cap on the answer cuts work in half, a summary
loses detail, a truncation loses more. This page is how a Dot decides, and the
failures that decided it.
## What went wrong with fixed limits
Until October 2026 a Dot sent `max_tokens: 4096` on every request and kept each
thread under a configurable 32,000 tokens. On long benchmark tasks (writing a
Scheme interpreter, a key-value server: see
[Benchmarking an agent on real VMs](benchmarking-agents-on-real-vms.md)) this
failed in a specific way: the model wrote a whole program in one `write_file`
call, the answer was cut at 4,096 tokens in the middle of the call, the engine
asked it to "continue from its exact endpoint", and with nothing delivered to
continue from, the model made the same call again until the task failed. A
model with a one-million-token window was being summarized at 32,000.
So the limits went, all of them: a Dot has no token setting. Every request uses
its model's own limits, read once from OpenRouter's list of models (the window
and the longest answer of the provider OpenRouter routes to by default), and
asks for the longest answer the window leaves room for. Without `max_tokens`
each provider would apply a default of its own, often far lower. A response
cut before it delivered anything is now told that nothing ran and to work in
smaller steps, instead of to continue.
## When to compact
A thread is compacted only when the next request would not fit: at the window,
less the room kept for the answer (20,000 tokens, or the longest answer when
that is shorter) and a safety margin. That is opencode's rule. How the prompt is
measured turned out to matter as much as the rule: see
[Token counts are not portable between models](token-counts-across-models.md).
## How: clear first, summarize second
1. **Old tool results are cleared.** Walking back from the newest message, the
first 40,000 tokens of tool results stay; older ones become a short note that
the tool can be called again, if that frees at least 20,000 tokens. The calls,
the reasoning and every message stay. No model call is needed. On SWE-bench
this costs about half as much as summarizing for the same solve rate
(JetBrains, "The Complexity Trap", arXiv 2508.21433); the thresholds are
opencode's.
2. **Only if that is not enough, a summary.** It is written as a handoff for the
model that resumes the work (Codex's framing), under the headings of
OpenHands' summarizing prompt, plus what only a Dot has: approvals given and
files written. After it, the person's latest messages are kept as they wrote
them (Codex keeps 20,000 tokens; a Dot keeps at most a quarter of the budget,
because a model with a small window still needs room after them). The summary
may be written by a cheaper model (`models.summary`), within that model's own
window; a thread too long even for it loses its oldest messages first, never
a call without its result.
OpenRouter's own `context-compression` plugin was looked at and not used: it is
a middle-out truncation, which drops the middle of the thread without a trace.
## What it changed
On the five long tasks (two to three hours each), the key-value server and the
Scheme interpreter went from failing most runs to passing 2 out of 2; all five
passed in the final round, in runs of 18 to 75 minutes.
One side effect is worth knowing: OpenRouter holds the cost of `max_tokens` up
front, so asking for the longest answer makes a nearly empty balance show
sooner. When it refuses for credit, the Dot now shows OpenRouter's own message
(how much the balance still allows, where to add credit) after its own.
## Sources
- opencode: [code](https://github.com/sst/opencode) (compaction thresholds)
- Codex: [code](https://github.com/openai/codex) (handoff summary, recent user messages)
- OpenHands: [code](https://github.com/All-Hands-AI/OpenHands) (summarizing condenser)
- JetBrains, "The Complexity Trap": [arXiv 2508.21433](https://arxiv.org/abs/2508.21433)
+118
View File
@@ -0,0 +1,118 @@
---
title: "How a Dot remembers"
description: "Past conversations kept as files the agent greps, a profile of the person in every prompt, and a background pass that keeps it current. Measured on LongMemEval: 7.8% to 90.5%."
parent: "Studies"
nav_order: 1
---
# How a Dot remembers
A Dot talks to one person for weeks: in its chat, and in the tasks it is
given. Three things make it remember what it was told, and each one was
added because a measurement asked for it.
| Layer | What it holds | Who writes it | In the prompt? |
|---|---|---|---|
| Conversations | every word said, by day and by task | the engine, after every turn | no: the Dot searches it with grep |
| `MEMORY.md` | what the Dot should always know about the person | the Dot, and a pass in the background | always |
| Notes and skills | details by subject, and how to do things | the Dot | when it opens them |
## The measure: LongMemEval
[LongMemEval](https://github.com/xiaowu0162/LongMemEval) (ICLR 2025, MIT)
gives each question a history of about 48 dated chats, around 115,000
tokens, and then asks something only that history answers: a detail the
person mentioned, one that changed later, how many days passed between two
events, what they would like, or something they never said (the right answer
is then that it is not known). The answer is graded by GPT-4o with the
benchmark's own prompts.
On Dots it runs as a person would meet it: a new Dot per question, the
history written as its past conversations, the question sent as a task, the
grading done outside the Dot so the OpenRouter key never enters it. 116
questions, drawn with a fixed seed in the benchmark's proportions; model
z-ai/glm-5.3-flash. The harness is `tests/bench/longmemeval.py`.
| | Correct | % |
|---|---|---|
| No history at all (the control) | 9/116 | 7.8% |
| Conversations as files | 96/116 | 82.8% |
| + the fixes that run found | 102/116 | 87.9% |
| + `MEMORY.md` from the memory pass | 105/116 | **90.5%** |
The control only gets the four questions whose answer is "you never told
me", and five it can guess.
## Conversations as files
The transcripts live in the engine's database, which the model's commands
cannot read, and a long chat reaches the model only as a summary of its older
part. So after every turn the engine also writes what was said to the Dot's
own computer:
```
/home/dot/conversations/chat/2023-05-20.md one file a day of the chat
/home/dot/conversations/tasks/2023-05-21-t1.md one file a task
```
Each message sits under a heading with its time, and each call the Dot made
is one line (not what it returned). The prompt says to search there with grep
when something said before matters. This one change took the Dot from 7.8% to
82.8%. Plain files and grep were chosen over embeddings or a vector store
because that is what wins in the published measures: in "Is Grep All You
Need?" grep beat vector search in every harness and model tried, and on
LongMemEval-V2 the best result came from an agent searching files (74.9%)
rather than from extracted facts with retrieval (58.6%).
## What the first run found
Twenty answers were wrong, and reading each one showed where:
- **4 of the 5 preference misses searched nothing.** Asked "can you suggest
accessories for my phone?", the Dot gave generic advice: nothing in the
question says "remember". The prompt now says to look in past conversations
before advice or a recommendation.
- **One task was refused by OpenRouter** for asking an answer 2,779 tokens
too long for the window: the token estimate was off. See
[Token counts are not portable between models](token-counts-across-models.md).
- The system prompt carried the time to the minute, so it changed on every
turn and the provider's cache never held. It now carries the day.
Together: 82.8% to 87.9%.
## MEMORY.md, and the pass that keeps it
The remaining preference misses had a common cause grep cannot fix: the fact
that matters shares no word with the question. "I've been sneezing, could it
be my living room?" needs "my cat Luna sheds", and a search for "sneez|dust"
does not find the cat. What fixes that is a short profile of the person in
every prompt, so the Dot does not have to guess what to look for.
`MEMORY.md` is that profile: who the person is, the people and animals in
their life, what they have and use, what they like and dislike, their
routines and plans, each fact with the day it was said, and a line for every
other note. The Dot sees it whole in every prompt, up to 25,000 characters
(cut there, with a word to shorten it, as Claude Code does with its own).
A model often does not think to write it while it works, so the engine keeps
it current itself: once the Dot has been quiet for five minutes, it takes the
conversation files that changed since the last time and has the summary model
rewrite `MEMORY.md` from them, in one request with no tools. A changed fact
replaces the old one with its date; nothing a web page or a command said is
copied in as an instruction. If the conversations do not fit one request they
go in several, oldest first. A pass that is cut, empty, unpriced, or races
with the Dot editing the file writes nothing, and the next one takes the same
conversations again. Its cost goes to the person's usage like any other turn.
On the 30 preference questions it raised the right answers from 65% to 83%,
and on the 116 mixed ones updates went from 78% to 94% (the profile keeps the
newest version of a fact), with nothing else moving beyond the noise of a run.
How this was chosen among five methods is in
[Agent memory systems, measured on the same questions](agent-memory-systems-compared.md).
## What it costs
With glm-5.3-flash a pass over a whole 115,000-token history costs about
$0.015. In use a pass reads only what changed since the last one, so a normal
day of chat comes to about a cent. Answering a question that needs the
history cost the Dot $0.003 to $0.005 on average.
+33
View File
@@ -0,0 +1,33 @@
---
title: "Home"
nav_order: 1
---
# invisible_dots Wiki
invisible_dots runs Dots: autonomous AI agents, each with its own Linux
computer, that work for a person through chat and tasks. This wiki is where
the work behind them is written up: what was measured, how, what came out,
and what was chosen because of it. Every study has its numbers and its
method, so a reader can check them or run them again.
The [README](https://github.com/feder-cr/invisible_dots#readme) is how to run
invisible_dots; [the architecture](https://github.com/feder-cr/invisible_dots/blob/main/docs/architecture.md)
is how it is built.
## [Studies](studies.md)
- [How a Dot remembers](how-a-dot-remembers.md): past conversations as files,
a profile in every prompt, and the pass that keeps it current. 7.8% to 90.5%
on LongMemEval.
- [Agent memory systems, measured on the same questions](agent-memory-systems-compared.md):
Mastra, LangMem, Hindsight, an agentic "dream" pass and a single request,
with their cost per pass.
- [Token counts are not portable between models](token-counts-across-models.md):
tiktoken against five models' own counts, and why a fixed safety margin got
a request refused.
- [Compacting a thread at the model's own window](compacting-at-the-models-window.md):
no token cap of our own, old tool results cleared before anything is
summarized.
- [Benchmarking an agent on real VMs](benchmarking-agents-on-real-vms.md):
Harbor tasks, two-to-three-hour builds, and eight Dots on one host.
+18
View File
@@ -0,0 +1,18 @@
---
title: "Studies"
has_children: true
nav_order: 2
---
# Studies
What was measured to decide how a Dot works. Each page says the question, the
method, the numbers, and what was chosen; the code and the harness that
produced the numbers are in the repository, under `tests/bench/` and
`invisible_engine_dots/`.
- [How a Dot remembers](how-a-dot-remembers.md)
- [Agent memory systems, measured on the same questions](agent-memory-systems-compared.md)
- [Token counts are not portable between models](token-counts-across-models.md)
- [Compacting a thread at the model's own window](compacting-at-the-models-window.md)
- [Benchmarking an agent on real VMs](benchmarking-agents-on-real-vms.md)
+76
View File
@@ -0,0 +1,76 @@
---
title: "Token counts are not portable between models"
description: "tiktoken against the counts of five models on OpenRouter, by kind of text: from 0.52 to 1.33 times. Why a fixed safety margin got a request refused, and how a Dot sizes its requests now."
parent: "Studies"
nav_order: 3
---
# Token counts are not portable between models
Before an agent sends a request it has to know how big the prompt is: to
decide when to compact a long thread, and how long an answer it may ask for
(the prompt and the answer together must fit the model's window). Only the
provider knows its model's tokenizer, so engines estimate, usually with
OpenAI's tiktoken, and leave a small margin. A Dot did this with a margin of
1,024 tokens, and a long task on a one-million-token window was refused.
## The refusal
A memory task on z-ai/glm-5.3-flash (window 1,048,576 tokens, longest answer
943,717) asked OpenRouter for the longest answer that fitted after a prompt
the engine had estimated at 103,346 tokens. OpenRouter answered:
> This endpoint's maximum context length is 1048576 tokens. However, you
> requested about 1049513 tokens (100506 of text input, 5290 of tool input,
> 943717 in the output).
Its count of the prompt was 105,796: the estimate was 2,450 tokens (2.4%)
short, more than twice the margin, and the request overflowed the window by
937 tokens.
## The measurement
Six kinds of text were sent to five models through OpenRouter, asking for a
one-word answer, and the prompt tokens each model reported were compared with
tiktoken's count of the same request (ratio counted / estimated):
| Model | English chat | Python | TypeScript | JSON | Italian | Chinese |
|---|---|---|---|---|---|---|
| anthropic/claude-sonnet-4.5 | 1.115 | **1.334** | 1.242 | 1.119 | 1.040 | 1.097 |
| google/gemini-2.5-flash | 1.039 | 1.275 | 1.172 | 1.051 | 0.880 | 0.548 |
| deepseek/deepseek-chat-v3.1 | 1.008 | 1.120 | 1.066 | 1.004 | 0.920 | 0.516 |
| qwen/qwen3-235b-a22b-2507 | 1.020 | 1.001 | 1.011 | 1.028 | 0.960 | 0.549 |
| openai/gpt-4o-mini | 0.990 | 1.005 | 1.007 | 0.989 | 0.920 | 0.774 |
(October 2026; texts of 8,000 to 28,000 tokens.) The spread runs from about
half to a third more, and it depends on the model **and** on the text: Claude
counts Python a third above tiktoken, the Chinese text is counted at half by
four of the five. A margin of fixed size cannot cover a relative error: on a
600,000-token prompt of code, a third is 200,000 tokens.
## Why the provider's own count is not enough either
The obvious fix is to use what the provider reports: every response says how
many prompt tokens it took, so the next request is that count plus an estimate
of what was added. It was built, and the refusal came back. OpenRouter checks
a request against the window with **a count of its own**, made before it routes
the request and reported only in the refusal; on glm-5.3-flash it ran above the
model's count plus a scaled estimate of the rest. The judge is a number the
engine never sees on a request that succeeds.
## What a Dot does now
The whole estimate is scaled: a prompt is taken for its tiktoken count times
1.35 (the largest ratio measured above, with room), or times the largest ratio
the model's own counts have shown when that is more (learned from every
response of more than 1,000 tokens). Being wrong upwards costs little: an
answer limit of 900,000 tokens instead of 943,717, and a thread compacted a
little earlier. Being wrong downwards fails the request. Each request now logs
its size, how it was measured and the answer limit it sent, so the next
surprise shows its numbers.
A related fix: the system prompt carried the time to the minute, so it changed
on every turn, and neither the provider's prompt cache nor any count of the
previous request could carry over. It carries the day now.
Code: `invisible_engine_dots/nanobot/providers/prompt_count.py`.