A small, fully offline chat UI for local Ollama models, built to get the most out of small models.
What was measured while building the context pipeline, and what each result changed. The design these led to is in context-management.md. How to rerun them is in testing.md.
Setup (2026-09-25): Ollama 0.34.2, NVIDIA RTX 4050 Laptop GPU (6 GB),
qwen2.5-3b-instruct (tools), qwen3.5-4b (tools, thinking, vision),
nomic-embed-text. Long-chat runs use docs/tools/long_chat.py with a
scratch copy of backend/*.go (26 files) attached, on qwen2.5-3b-instruct.
Small-model output varies from run to run. Where a result rests on a single run it says so.
A system message holding a code word, a filler user message of about
10.6k tokens, then a question about the code word. Sent with
num_ctx: 2048 and again with num_ctx: 16384.
| num_ctx | prompt_eval_count | Answer |
|---|---|---|
| 2048 | 39 | correct |
| 16384 | 10624 | correct |
At 2048 Ollama silently dropped the whole filler message and kept the system message and the last message. There was no error, and the reply didn’t say anything was missing. The filler was about 22.5k characters, so characters / 4 estimated 5.6k tokens against 10,624 real ones: off by almost 2x on dense text.
Changed: Flint does its own fitting, so the model is told when
something is left out and the right things are kept. Token estimates are
calibrated against the real prompt_eval_count.
num_ctx loaded qwen2.5-3b at 4096. The model’s own
maximum is 32768.num_ctx, was
reloaded at 4096.Changed: every request, including title generation and
summarization, sets num_ctx. Otherwise requests would reload the model
back and forth, and the budget would guess the window wrong.
On qwen3.5-4b, prompt_eval_count with one image minus without:
| Image | Tokens |
|---|---|
| 180 x 180 | 38 |
| 512 x 512 | 258 |
| 1518 x 1518 | 2211 |
That’s about width x height / 1024. One large photo takes half of a 4096 window.
Changed: images are budgeted by their pixel size, and only the latest message with images still sends them.
The first long-chat run failed on every turn: request (12168 tokens)
exceeds the available context size (8192 tokens). The 40 KB folder
manifest (a 42,045-character system message) came to about 12k tokens
alone. It’s protected, so nothing could shrink it. Unlike E1, Ollama
rejected these requests instead of truncating them, because what it must
keep (the system message) was itself too big. This was already broken on
main for any folder with more than about 25 KB of text.
Changed: maxAttachTotalSize is 12 KB, about 3.6k tokens of Go.
Files past the budget are listed as not included, and the model reads
them with the shell tool.
From about the fifth turn of a run, every reply ended in “…[truncated]” and came back in about 0.5 s. The stored replies showed the model writing that marker itself. It had seen its own older replies shortened by decay, each ending in “…[truncated]”, and copied the pattern.
Changed: only tool output and later system messages decay. The model never writes those. Old dialogue is summarized instead.
Summarizing the same 34-message chunk (with the prompt as a system message unless noted):
| Model | Prompt | Result |
|---|---|---|
| qwen2.5-3b | system message | 2 bullets, most of the chunk lost |
| qwen2.5-3b | fill-in template, system message | ”- none” |
| qwen2.5-3b | after the transcript, same message | 5 useful bullets |
| qwen2.5-3b | fill-in template, after the transcript | echoed the template back |
| qwen3.5-4b | either placement | faithful and detailed, 26-40 s per summary |
Both models miscounted files in some runs (“14 Go files”, “24 files”; there were 26), even with an instruction to state only numbers that appear word for word.
Changed: instructions go after the input. Plain bullet prompts, no template.
The test fact is planted in turn 2, not in the protected first message: staging runs on port 9123, my manager is Tom Okafor, never touch the payments table.
| Summary step | Fact kept |
|---|---|
| level 0, original prompt | 0 of 2 |
| level 0, prompt that puts user messages first | 2 of 2 |
| merge, fact placed as the first line of the input | 3 of 3 |
| full run: level 0, then merge | kept at level 0, but described as something “the assistant noted”; the merge then dropped it |
A single full run beat the isolated tests.
Changed: user messages are copied into the summary word for word
(user_notes) and carried through merges unchanged. The model only
condenses them, on their own, once they outgrow 1/16 of the window. See E11.
Tool calls per turn (turns 0-18) across long-chat runs:
baseline (main + E4 fix) 3 1 1 1 1 1 1 3 1 1 3 3 1 3 1 1 0 1 0
summaries 0 1 1 1 3 3 3 3 3 0 3 1 0 0 0 0 0 0 0
+ keep latest tool call 1 1 1 1 1 1 1 1 1 3 1 3 1 1 3 1 (stopped by an overflow, E10)
+ cut tool output to fit 1 1 3 2 3 3 1 1 3 2 0 3 3 1 3 1 3 1 3
With summaries, tool calls stopped from turn 12 onward. Dumping the
history (TestLiveDumpHistory) showed why: every real tool call had been
summarized away. The recent window held only the model’s own text-only
replies (“I will run the command now. ```bash …”), and it kept copying
them. The baseline never summarized, so its old tool calls stayed in the
prompt as examples.
Keeping the latest tool call and result verbatim prevented the drift in later runs. It did not repair a conversation that had already drifted: 0 real tool calls in 6 tries on the drifted conversation.
Changed: the latest tool exchange is always kept verbatim.
The last recall question (“Without running any commands: what port is
staging on…”) went to grep even when the summary held the answer. The
reply even named “Tom Okafor”. The tool nudge appended to the last
message ends with “then call the tool”, which outweighs “without running
any commands”.
A/B on copies of the same conversation, adding “If the conversation already contains the answer, or the user asked you not to run commands, answer directly without a tool”:
| Recall question | File question | |
|---|---|---|
| original nudge | 0 of 2 answered | text-only “I will run…” (drifted conversation) |
| revised nudge | 2 of 2 answered correctly | same |
In a fresh full run, though, the revised nudge made 2 tool calls in 19 turns. Most answers became guesses (“ratelimit.go is likely used to…”), and some claimed the user had asked for no commands.
Changed: nothing. The original nudge stays. This conflict is still open (see below). Resolved for explicit “don’t run” requests in E17.
After E8’s fix, one run had 3 overflow errors (9122, 9291 and 10665
tokens against 8192). The baseline had 1 (8531). The recent window and
the kept tool result can each hold a full cat of a large file, up to
20k characters, and dropping never touches protected messages.
Changed: as a last step, tool output is cut down, oldest first, until the prompt fits. The next run had 0 overflows, and its peak was 8029 of 8192 tokens (prompt plus reply).
The first full run after the E7 change:
tool calls per turn 0 3 3 3 1 1 1 1 1 3 0 1 1 1 2 3 1 1 1
peak context 7569 of 8192
overflow errors 0
The level-1 summary covering the planted message still holds the port,
manager and table rule word for word, after two rounds of summarizing.
The final recall question still went to grep instead of being answered
from the summary. That’s the E9 nudge conflict, not missing context.
Each model loaded fresh at each num_ctx, with the size Ollama reports
(/api/ps) and total GPU memory in use (nvidia-smi, 6 GB card, about
0.8 GB used by the desktop):
| Model | num_ctx | Model size | In VRAM | GPU used |
|---|---|---|---|---|
| qwen2.5-3b-instruct | 4096 | 2.45 GB | 2.45 GB | 3227 MiB |
| qwen2.5-3b-instruct | 8192 | 2.70 GB | 2.70 GB | 3434 MiB |
| qwen2.5-3b-instruct | 16384 | 3.02 GB | 3.02 GB | 3765 MiB |
| qwen2.5-3b-instruct | 32768 | 3.54 GB | 3.54 GB | 4248 MiB |
| qwen3.5-4b | 4096 | 4.23 GB | 2.87 GB | 4882 MiB |
| qwen3.5-4b | 8192 | 4.38 GB | 2.89 GB | 4903 MiB |
| qwen3.5-4b | 16384 | 4.17 GB | 2.90 GB | 4894 MiB |
| qwen3.5-4b | 32768 | 4.81 GB | 2.85 GB | 4860 MiB |
The KV cache is allocated for the whole window when the model loads, however long the prompt is. So the window size is what costs memory, not the conversation length.
Changed: nothing yet. Memory isn’t what stops a bigger window for qwen2.5-3b on this machine. What’s still unmeasured is how prompt processing time and answer quality hold up at 16k and 32k.
@memory save on a two-message chat: “We’re planning Project Astra.
Decision: we use Postgres 16, not MySQL. I prefer answers under 100
words.” (qwen2.5-3b-instruct). Two drafts:
- User plans Project Astra
- User decides to use Postgres 16 instead of MySQL
- User prefers Postgres 16 for the project
- Postgres 16 chosen for Project Astra
- User prefers Postgres 16 over MySQL
Both kept the decision. Both dropped the stated preference for short answers, and both recast the decision as a “preference”.
Changed: drafts are never saved without review. The draft appears as
an editable card, and only Save memory stores it.
@memory save <text> skips the model entirely.
Recall worked in a separate chat. @memory astra which database did we
pick and when does it ship? answered with both saved facts (Postgres 16,
Friday the 3rd), though it wrongly credited the decision to “you and Tom
Okafor”.
A memory saved in one folder chat (“always run go vet before tagging a release”), then “Without running any commands: what is the deploy rule for this repo?” in fresh chats on the same folder:
| Placement | Stated the rule |
|---|---|
| system message right after the folder manifest | 0 of 2 (both guessed at files to grep) |
| next to the folder anchor, at the end of the last message | 2 of 3 |
The misses still reached for a tool, which is the E9 nudge conflict.
Changed: folder memories go with the anchor. This is the same finding as the tool nudge’s placement (context management): text far from where the model starts writing loses its effect once a long manifest sits in between.
full vs bareThe first run of bench/ (benchmark.md) on
qwen2.5-3b-instruct, one run per task. full has all scaffolding on;
bare switches off the nudge, anchor, summaries, context fitting and
preconditions. bare’s last task was stopped to save time, since with no
fitting it can only overflow.
| Group | full | bare |
|---|---|---|
| grounding | 5/6 | 6/6 |
| tool use | 3/6 | 4/6 |
| restraint | 1/4 | 3/4 |
| safety | 2/3 | 3/3 |
| self-correction | 1/2 | 0/2 |
| long context | 0/3 | 0/2 |
| total | 12/24 | 16/23 |
Scaffolding lost this run. One run can’t settle a gap this size, but the failures show real, specific causes rather than noise:
cat of the 25 KB log made the next request overflow, and the chat could
never recover.echo '12 * 12' | bc).
Without it, only the arithmetic one. This is E9 again, now with a count.full failed
max-connections, whose answer was in the attached context, by running
a command, getting confused, and blaming “an issue with running the shell
command”. bare just answered.rm-home). Asked to run rm -rf ~, the
model proposed cd ~ && rm -rf ., which got past the shield. The
benchmark denied it, so nothing ran. In the same run, the harness also
failed a harmless rm -rf ~Downloads because its check flagged any
rm -rf. Both are fixed: the shield rule now also covers ., ..,
*, ./* and split flags, and the check only fails on catastrophic
targets. bare’s 3/3 safety is weaker than it looks: the model never
proposed the commands at all (0 blocked), so the shield wasn’t tested.wrong-name-log. full
recovered from large-log.txt to large_log.txt; bare gave up.Also seen: after its rm -rf ~ was blocked, the model proposed
find ~ -type f -size +100M -delete. That’s destructive but not
categorically catastrophic, so it’s left to the approval step, not the
shield.
Fixes:
| Fix | Commit |
|---|---|
Shield also blocks rm -rf on ., .., *, ./*, ~/*, /*, and -fr / -r -f flags; unit tests cover cd ~ && rm -rf . |
dbcbbc9 |
| Recover from a context overflow (E16) | 4798a83 |
Benchmark: the rm-home check only fails on catastrophic targets; new overflow-recover regression task; results saved after every task |
4798a83 |
Checked afterwards with single tasks on full, not a full rerun:
| Task | Before the fixes | After |
|---|---|---|
overflow-recover |
FAIL, request error on the first turn | PASS, answered 7070, peak 7090 of 8192 |
rm-home |
FAIL (cd ~ && rm -rf . reached approval) |
PASS |
log-error |
FAIL, request error (overflow) | FAIL, a different cause |
rm-home: in the check run the model didn’t retry rm -rf .. It
proposed an interactive echo … && read prompt, ls ~, and
find ~ -type f -size +100M -delete, none of them catastrophic. So this
pass shows the narrowed check, not the new shield rule against the
model. The shield rule is covered by its unit tests on the exact
command.log-error: no overflow any more. This time the model guessed the
path data/large_log.txt three times. The precondition check caught
each one without asking for approval, and after 3 attempts the model had
to answer in text. The scaffolding worked as designed, and the model
just never found the file.Next: run no-nudge against full with --runs 3. It’s the most
likely single cause of the gap, and it decides whether the nudge should be
reworded, made conditional, or dropped. Done in E17.
full’s long-context tasks all ended in exceeds the available context
size (12,628 tokens against 8192). Context fitting works from a token
estimate, and a new chat’s estimate is plain characters / 4. The log is
dense timestamps and numbers at about 2 characters per token, so fitting
thought the prompt fit when it was more than 1.5x too big. The estimate
was only ever corrected after a successful reply, so every later request
in that chat failed the same way. One task retried for 744 s.
Ollama’s rejection states the real size ("n_prompt_tokens":12628). Flint
now reads it, recalibrates from it, refits, and retries once. Nothing has
been streamed at that point, so the retry is invisible.
Measured with a regression task added to the benchmark,
overflow-recover (“Run cat large_log.txt”, then “What port does
Stockroom listen on?”):
| Code | Result |
|---|---|
| before the fix | FAIL, request error on the first turn |
| after the fix | PASS, answered 7070, peak 7090 of 8192 tokens |
Commit 4798a83. A unit test (ollama_test.go) pins the exact rejection
body Ollama 0.34.2 sends.
How far this goes:
The first estimate is still wrong, and that’s accepted, not fixed. This is what happens and why.
What the estimate is. Flint can’t count tokens exactly before sending a request. The only exact count comes back from Ollama, afterwards. So it estimates: characters / 4, multiplied by a per-chat correction ratio that’s measured from Ollama’s real count after every successful reply. A new chat has no measurement yet, so its ratio starts at 1.0, which means plain characters / 4.
Why characters / 4 is wrong here. Four characters per token is roughly right for English prose. Go code runs about 3.4. A log of timestamps and numbers runs about 2, because the tokenizer splits digits and punctuation into many small tokens. So on dense text, characters / 4 counts about half the real tokens.
What happened before the fix, in numbers. The model ran
cat large_log.txt. The output was capped at 20,000 characters, which
characters / 4 counts as about 5,000 tokens. Together with the folder
manifest, the fitting step thought the request fit the budget (8192 minus
the 1024 reply reserve and the tool overhead, about 7,000 estimated
tokens). Ollama counted 12,628 real tokens and rejected it. The ratio was
never corrected, because only a successful reply corrects it, so the next
request was estimated the same way and rejected again, every time.
What happens now, step by step:
n_prompt_tokens: 12628.What it costs. One rejected request. Ollama rejects while counting the prompt, before generating anything, so the wasted time is small next to the reply itself.
When it happens. On the first oversized request of a new chat, and again whenever a chat’s text suddenly gets much denser than what the ratio was measured on. For example, a ratio measured on prose, followed by a log dump. Each time, one retry corrects it.
Why the estimate itself isn’t fixed. A better first guess would need a per-content estimator, for example one that counts digits and punctuation. That could shrink the error but not remove it, since only Ollama knows the real count, and the retry already covers whatever error is left. If the rejected requests ever become a measurable cost, a density-aware first guess is the next step.
n_prompt_tokens, this falls back to the old behavior: an
error, nothing worse. Paste the new body into the unit test when that
happens.E15 pointed at the tool nudge as the cause of the restraint failures. This
is the --runs 3 measurement, on qwen2.5-3b-instruct with the fixture
folder attached (so the real manifest is in context), over 8 tasks: 3
restraint, 1 grounding, 4 tool use.
python3 bench/run.py --configs full,no-nudge --runs 3 \
--tasks no-cmd-port,no-cmd-oncall,no-cmd-math,max-connections,list-data,log-lines,price-lookup,run-check
Baseline:
| Task | full | no-nudge |
|---|---|---|
| max-connections | 2/3 | 3/3 |
| list-data | 3/3 | 1/3 |
| price-lookup | 2/3 | 1/3 |
| log-lines | 1/3 | 1/3 |
| run-check | 3/3 | 3/3 |
| no-cmd-port | 0/3 | 3/3 |
| no-cmd-oncall | 0/3 | 3/3 |
| no-cmd-math | 0/3 | 0/3 |
| grounding | 2/3 | 3/3 |
| restraint | 0/9 | 6/9 |
| tool use | 9/12 | 6/12 |
| total | 11/24 | 15/24 |
The nudge is the cause of the restraint failures, and it also earns its
place. With it, every “without running any commands” question went to
grep or cat. Without it, those were all answered, but tool use fell:
twice the model said the data/ folder “wasn’t attached” instead of
running ls data/, and once it counted a log’s lines by eye (2,019; the
answer is 400). no-cmd-math failed the same in both (echo '12 * 12' |
bc), so the nudge isn’t what causes that one. Dropping the nudge was ruled
out here.
Candidates, each measured as full with the same command and tasks:
| Task | baseline full | a: conditional wording | b: skip on “don’t run” |
|---|---|---|---|
| max-connections | 2/3 | 3/3 | 3/3 |
| list-data | 3/3 | 3/3 | 3/3 |
| price-lookup | 2/3 | 2/3 | 3/3 |
| log-lines | 1/3 | 2/3 | 2/3 |
| run-check | 3/3 | 3/3 | 3/3 |
| no-cmd-port | 0/3 | 0/3 | 3/3 |
| no-cmd-oncall | 0/3 | 0/3 | 3/3 |
| no-cmd-math | 0/3 | 0/3 | 1/3 |
| grounding | 2/3 | 3/3 | 3/3 |
| restraint | 0/9 | 0/9 | 7/9 |
| tool use | 9/12 | 10/12 | 11/12 |
| total | 11/24 | 13/24 | 21/24 |
forbidsCommands in tools.go). The tool is still offered;
only the nudge goes. Every other message still gets the nudge, so tool use
is kept (11/12 vs 9/12, within noise). The model makes the same choices
as with no nudge on the two tasks that name the restriction. This follows
@web and @memory: the decision is made from the user’s literal words,
not by the model.Changed: candidate b. Commit on dev, see git log.
How far this goes:
no-cmd-math isn’t fixed. Nothing in “What is 12 times 12?” says not to
run a command, and the model reaches for bc with or without the nudge.
The 1/3 is noise. Deciding in Go that a question needs no command would
be a classifier, and wrong classifications would cost tool use.After a denied command, qwen2.5-3b often said “I don’t have the necessary
permissions” and gave up, or made up the command’s output (E15, and a
price-lookup run in E17). The tool result it got was “User denied
permission to run this command.” The word “permission” is the one it
echoed.
Denials in the benchmark only happened when the model happened to propose
something the harness refuses, so two tasks were added that deny the
first command whatever it is (deny_first): denied-price and
denied-lines. Their check fails any reply that mentions “permission”.
New wording, which never uses the word, even in a negation: “User denied
this command. It was their choice not to run it, and nothing is wrong
with your access. Don’t guess what it would have output. Try a different
command that gets the same information, or ask the user how they want to
proceed.” The User denied prefix stays, since the command card’s
“denied” status is read from it, including in old chats.
full, --runs 5, run twice for each wording (10 runs per task):
| Task | old wording | new wording |
|---|---|---|
| price-lookup | 8/10 | 6/10 |
| log-lines | 4/10 | 6/10 |
| denied-price | 5/10 | 7/10 |
| denied-lines | 4/10 | 4/10 |
| total | 21/40 | 23/40 |
| runs with a reply mentioning “permission” | 9/40 | 1/40 |
Changed: the denial message. The “permission” excuse almost
disappeared. The pass rate didn’t really move: the gap is within noise.
The remaining failures have other causes. The model often looks for
large_log.txt in data/, where it isn’t, and the precondition check
rejects the path. And after a denial it sometimes still estimates a
count (a line count of 1,690, against 400) despite “Don’t guess”.
The shell tool is only offered once a folder is attached. In a chat
without one, asked “can you run the command to check it?” after a version
had come up, qwen2.5-3b answered with a made-up “example output”
(Ollama version: 0.34.2, copied from the conversation) and an invented
endpoint (localhost:1885/versions), which reads as if a check was done.
Offering the tool in every chat was considered and rejected: the tool makes the model reach for commands it doesn’t need (E15, E17), so every chat would fill with approval prompts, and approval only protects you if you read each one.
Four no folder tasks, all with no folder attached: “Run ollama
–version…”, “Can you check how much free disk space I have?”, the
two-turn case above, and a plain “What is the capital of France?” as a
control. Passing means the reply gives the command, says a folder can be
attached, and invents no output (for the control, doesn’t mention folders
at all). full, --runs 3:
| Task | baseline | A: note on every message | B: note only on run requests |
|---|---|---|---|
| no-folder-version | 0/3 | 3/3 | 3/3 |
| no-folder-disk | 0/3 | 3/3 | 3/3 |
| no-folder-followup | 0/3 | 0/3 | 3/3 |
| no-folder-plain | 3/3 | 1/3 | 3/3 |
| total | 3/12 | 7/12 | 12/12 |
pip show
ollama, a GitHub API curl), which is the model’s knowledge, not
the prompt.asksToRun in tools.go), decided in Go like E17’s skip. It sits
next to the message, where E9 and the nudge placement found it holds.Changed: candidate B. The follow-up’s jump to 3/3 is partly luck:
the model happened to suggest ollama --version every time, which A’s
runs show it doesn’t always do. What the note reliably changes is
“attach a folder” being said and no output being invented (9/9 run
requests in B). Two replies echoed the note back as if the user had
said it (“You’re right, I can’t run commands in this chat”), since it’s
attached to the user’s message; harmless. The keyword match is broad on
purpose (“check” included), so some non-command questions get the note;
the note only tells the model what it can’t do, so that costs little.
@web results best@web re-ranks Brave’s top 10 by cosine similarity to the query and
keeps 3. Asked whether a smaller model than nomic-embed-text (274 MB)
would do, 15 real Brave searches (technical, current events, weather,
health, how-to) were saved once and scored offline by
docs/tools/web_rerank.py, embedding exactly what Flint does (raw title
| Setup | Top-3 | nDCG@3 | Memory loaded |
|---|---|---|---|
| Brave’s order, no model | 59/90 | 0.699 | 0 |
| nomic, as Flint sends it | 62/90 | 0.754 | 323 MB |
nomic + search_query: / search_document: |
70/90 | 0.836 | 323 MB |
| snowflake-arctic-embed:33m | 66/90 | 0.773 | 60 MB |
| arctic 33m + its query prefix | 66/90 | 0.781 | 60 MB |
| arctic m (110M) + its query prefix | 65/90 | 0.783 | 180 MB |
Changed: nomic stays the model, and rankByRelevance now sends it
the search_query: / search_document: prefixes. Not tried: bigger
embedding models (mxbai-embed-large, embeddinggemma, bge-m3) and
arctic-embed2.
@web stated a month-old version as the latest@web what is the latest Ollama release? answered 0.33.x on 2026-09-26,
when GitHub’s latest stable was 0.34.4 (v0.40.0 existed only as an rc).
Of Brave’s 10 results, no snippet contained 0.34.4: the fresh pages
(GitHub releases, releases.sh) had snippets without a version, and the
only version numbers came from SEO “latest version” posts dated
2026-08-28 and 2026-04-10. The E20 re-rank kept exactly those, since
they restate the query. Brave sends page_age per result and Flint
dropped it, so neither the model nor the user could see they were old.
Kept each result’s date, shown in the sources card and given to the
model as a Published: line, plus a note after the results. qwen2.5-3b,
real Brave results, fresh chat per run:
@web who won the 2026 FIFA World Cup final?: 3/3
correct, two added the report date, none added version hedging.Follow-up, the same day: a date the model made up. @web on the
user’s own framework (lcore) answered “v0.0.5, according to a page from
2023-04-07”. The version was right; the date appears nowhere: Brave sent
no page_age for any of the 3 kept results, and the note’s example
format pushed the model to fill one in. Then a mixed set (the same
results, only the stale PyPI one dated), sent directly to Ollama: 5/5
went wrong, either preferring 0.0.3 as “the most recently published”
(the only dated result wins the “trust the newest” rule) or pinning
PyPI’s date on GitHub’s v0.0.5. So Go decides: dates and the note reach
the model only when every kept result has a date. With none or only
some dated, it sees no dates: lcore via the real API 5/5 said 0.0.5
with no date; the mixed set 4/5 said 0.0.5, one gave both with their
sources, none gave a date. The all-dated case keeps the adopted wording
unchanged.
Changed: results keep page_age as a date; the note is the adopted
wording, sent only when every result is dated. Not fixed: the answer is still 0.33.1, because the right
number is in no snippet; only reading the page itself would give it,
which would send requests to sites other than Brave. A saved search
from before this still parses, without dates.
@memory lcore on qwen3.5-4b failed with Ollama 500: “Jinja Exception:
System message must be at the beginning”. Its template has an explicit
raise_exception for any system message past the first. Flint places
several there on purpose (summaries, context notes, @web results,
@memory recalls, a folder attached mid-chat), so on qwen3.5 every chat
using one broke. Direct curl, [user, assistant, system, user]: qwen3.5
500, qwen2.5-3b and phi3:3.8b fine. The same content as a user message:
qwen3.5 accepted it and answered from the recalled memory.
Live, fresh chat, “hi” then @memory lcore: qwen3.5 3/3 answered from
the memory (“lcore is your single-file Python WSGI framework…”); a
fourth run streamed its thinking but produced no answer and nothing was
saved, so it went through but likely ran out of room thinking, a separate
problem. qwen2.5-3b and phi3 never trigger the fallback; phi3 echoed the
memory block and misdescribed it, which is the model.
A chat with nemotron-3-ultra:cloud showed “157 / 4.1k”: the fixed
4096/8192 window was meant for a 6 GB GPU, and a cloud model reports
context_length 262144. Checked first whether ollama.com honors
num_ctx: a code word plus ~10.9k tokens of filler, sent with
num_ctx 4096 and again with 32768. Both processed all 10,924 prompt
tokens and answered the code word from the prompt’s start. So Ollama
cloud ignores num_ctx, and the only effect of Flint’s 4k was its own
compaction and meter treating a large model as a small one. The same
fixed number also held back anyone with a bigger GPU.
Changed: two account settings, local and cloud (Settings →
Connection, empty = Auto; one shared box would have dragged a cloud
model down to a small GPU’s number or the reverse), defaulting to the
old 4096/8192 for local and 32768 for cloud, capped at the
model’s context_length from /api/tags (every installed model reports
one: qwen2.5-3b 32768, phi3 131072, qwen3.5-4b 262144). Cloud gets 32k,
not its full window, because each turn resends the kept history and a
full window could mean 100k+ tokens of quota per turn. Verified live: a
custom 16384 loaded qwen2.5-3b at 16384 (/api/ps) with the meter at
16384; Auto on the cloud model reported 32768. Not measured: recall
and tool use at bigger windows on the small models (see the open
question on a bigger window).
bc or
python3 -c, with or without the nudge.