Describe the bug
getContext (src/loop.metta) assembles the prompt with the most volatile section in the middle:
PROMPT · SKILLS · prompt-extensions · OUTPUT_FORMAT · SAVE_PERMANENT_FILES_DIR
· LAST_SKILL_USE_RESULTS ← changes completely every cycle, up to maxFeedback chars
· HISTORY ← re-sliced tail of memory/history.metta
· TIME
Providers with automatic prompt caching (OpenAI, DeepInfra, OpenRouter, …) reuse only a prefix
that is byte-identical to the previous request. The first byte that changes ends the reusable
prefix. Because LAST_SKILL_USE_RESULTS is both the most volatile section and one of the largest,
the reusable prefix ends after the static head — roughly the first 4 KB of a ~48 KB prompt — no
matter how stable everything after it is.
This also defeats work the repo has already done: providers/lib_llm_ext.py::_stable_cache_key is
used by providers/openai.py (prompt_cache_key) and providers/openrouter.py (session_id) to
pin cache identity across calls. Pinning the identity does not help while the payload's stable
part sits behind a volatile one.
There is a second, independent obstacle: getHistory returns the tail of an append-only file,
so the history window slides on every append and its bytes shift position each cycle. Reordering
alone therefore does not make history reusable — it needs a stable cut point as well.
To Reproduce
- Run with a provider that reports cached input tokens (we used DeepInfra and OpenRouter, model
z-ai/glm-5.2).
- Log per-call usage (
prompt_tokens, cached_tokens / prompt_tokens_details).
- Trigger a burst of consecutive cycles — e.g. send a message, so several loop iterations run
within the provider's cache TTL.
- Compare
cached_tokens against prompt_tokens.
Expected behavior
The reusable prefix covers everything that did not change since the previous call, so the cached
fraction is roughly proportional to the stable part of the prompt.
Actual behavior
On our instance the cached count sat at ~1,088 tokens per call — matching the static head almost
exactly — i.e. about 3% of input tokens were billed at the cached rate during multi-call
bursts, while ~40% of the prompt was in fact unchanged between those calls.
Measured section sizes from one production prompt (~48 KB total):
| section |
bytes |
share |
| static head (PROMPT + SKILLS + OUTPUT_FORMAT + SAVE_PERMANENT_FILES_DIR) |
4,208 |
8.8% |
LAST_SKILL_USE_RESULTS |
23,439 |
49.0% |
HISTORY |
20,157 |
42.1% |
TIME |
32 |
0.1% |
Suggested fix
- Minimal, no behaviour change: order the prompt stable → volatile, i.e. move
LAST_SKILL_USE_RESULTS after HISTORY, leaving TIME last.
- To make
HISTORY itself reusable: cut it at a fixed byte-block boundary instead of a sliding
tail. memory/history.metta is append-only, so the bytes below the largest block multiple
strictly under the file size never move; emitting that part as a stable "archive" block plus a
small recent tail keeps the archive byte-identical between block crossings, with no extra state
and no change to how much history the model sees.
We ran (1) + (2) behind an opt-in flag on our own instance: cached fraction went from ~3% to ~40%,
which at DeepInfra's rates ($0.93 vs $0.18 per Mtok input, an 80.6% discount on cached tokens)
came to roughly a 33% reduction in input cost on burst cycles. Cycles spaced further apart than
the provider's cache TTL naturally gain nothing.
All figures above are measurements from our own deployment, not synthetic estimates.
Note for #284 (Context Frames)
The same principle applies to the frames layout, and the current draft would hit it: the proposed
getContext puts CURRENT_CONTEXT_FRAME_S_EXPR near the top, where a frame that changes each
cycle would invalidate everything after it. Keeping per-cycle-mutable sections last preserves
whatever caching the static head and the frame-independent sections could otherwise provide.
Additional context
- OmegaClaw version: current
main
- Communication channel: Telegram
- Model provider: OpenRouter and DeepInfra (
z-ai/glm-5.2)
We have a working implementation of both parts, default-off so the flag-off prompt is
byte-identical to today's. Happy to open a PR if that would be useful.
Describe the bug
getContext(src/loop.metta) assembles the prompt with the most volatile section in the middle:Providers with automatic prompt caching (OpenAI, DeepInfra, OpenRouter, …) reuse only a prefix
that is byte-identical to the previous request. The first byte that changes ends the reusable
prefix. Because
LAST_SKILL_USE_RESULTSis both the most volatile section and one of the largest,the reusable prefix ends after the static head — roughly the first 4 KB of a ~48 KB prompt — no
matter how stable everything after it is.
This also defeats work the repo has already done:
providers/lib_llm_ext.py::_stable_cache_keyisused by
providers/openai.py(prompt_cache_key) andproviders/openrouter.py(session_id) topin cache identity across calls. Pinning the identity does not help while the payload's stable
part sits behind a volatile one.
There is a second, independent obstacle:
getHistoryreturns the tail of an append-only file,so the history window slides on every append and its bytes shift position each cycle. Reordering
alone therefore does not make history reusable — it needs a stable cut point as well.
To Reproduce
z-ai/glm-5.2).prompt_tokens,cached_tokens/prompt_tokens_details).within the provider's cache TTL.
cached_tokensagainstprompt_tokens.Expected behavior
The reusable prefix covers everything that did not change since the previous call, so the cached
fraction is roughly proportional to the stable part of the prompt.
Actual behavior
On our instance the cached count sat at ~1,088 tokens per call — matching the static head almost
exactly — i.e. about 3% of input tokens were billed at the cached rate during multi-call
bursts, while ~40% of the prompt was in fact unchanged between those calls.
Measured section sizes from one production prompt (~48 KB total):
LAST_SKILL_USE_RESULTSHISTORYTIMESuggested fix
LAST_SKILL_USE_RESULTSafterHISTORY, leavingTIMElast.HISTORYitself reusable: cut it at a fixed byte-block boundary instead of a slidingtail.
memory/history.mettais append-only, so the bytes below the largest block multiplestrictly under the file size never move; emitting that part as a stable "archive" block plus a
small recent tail keeps the archive byte-identical between block crossings, with no extra state
and no change to how much history the model sees.
We ran (1) + (2) behind an opt-in flag on our own instance: cached fraction went from ~3% to ~40%,
which at DeepInfra's rates ($0.93 vs $0.18 per Mtok input, an 80.6% discount on cached tokens)
came to roughly a 33% reduction in input cost on burst cycles. Cycles spaced further apart than
the provider's cache TTL naturally gain nothing.
All figures above are measurements from our own deployment, not synthetic estimates.
Note for #284 (Context Frames)
The same principle applies to the frames layout, and the current draft would hit it: the proposed
getContextputsCURRENT_CONTEXT_FRAME_S_EXPRnear the top, where a frame that changes eachcycle would invalidate everything after it. Keeping per-cycle-mutable sections last preserves
whatever caching the static head and the frame-independent sections could otherwise provide.
Additional context
mainz-ai/glm-5.2)We have a working implementation of both parts, default-off so the flag-off prompt is
byte-identical to today's. Happy to open a PR if that would be useful.