Summary
Brain › Sync › Sync History renders every run with 0 tokens and $0.0000, and the header totals read 0 in / 0 out · $0.0000 total. The reader and the UI are correct — no writer anywhere puts a non-zero token or cost figure on a source-sync audit row. Underneath it is a wider gap: memory inference spend never reaches the host cost tracker at all, so it is also missing from openhuman.cost_get_dashboard and from budget accounting.
Problem
What happened — 12 sync runs listed, Items correct (50, 50, 0, 50…), Tokens 0 and Cost $0.0000 on every row.
What was expected — real provider-reported token counts and cost for runs that embedded 506 chunks against cloud voyage-3-large.
Impact
- The panel states a sync was free. That is the one answer indistinguishable from the truth, and it is never true for a cloud embedder.
AgentActivityPanel's monthly memory cost (openhuman.memory_sources_monthly_cost_summary) is $0.00 for the same reason.
- Memory spend is absent from the cost dashboard and from
CostTracker::check_budget, which agent/tinyagents/host/budget_gate.rs uses for budget refusal and accounting. Memory inference is currently unbudgeted and unreported.
Steps to reproduce
- Connect a Composio source (GitHub/Gmail/Notion/Slack) with a cloud embedder configured.
- Brain › Sources › Sync on that source; wait for the run to finish.
- Brain › Sync › Sync History → the new row shows
Items: 50, Tokens: 0, Cost: $0.0000.
- Settings › Agent Activity → monthly memory cost
$0.00.
Environment — dev build on main @ 2475ea342, tinymemory pin v1.16.0, macOS. Staging profile ~/.openhuman-staging/users/69dc8c37935c9a8c539b43f2.
Root cause
Four independent layers. All four must be addressed or the symptom survives.
L1 — host writer hardcodes zeros. crates/openhuman-core/src/memory/sources/run_history.rs:96-100 — HostRun::into_entry writes batches: 0, input_tokens: 0, output_tokens: 0, estimated_cost_usd: 0.0, and HostRun has no token fields to carry. Covers every Sync click, Apply-all and Composio run (10 of the 12 rows).
L2 — driver writer hardcodes the same. tinymemory crates/tinymemory-core/src/sources/sync.rs:182-184 and :228-230, the periodic folder/GitHub/RSS/web loop (the other 2 rows). Zero since the file was created at tinymemory v1.5.0.
L3 — usage is produced, then dropped mid-chain.
| stage |
state |
provider returns UsageInfo (tinymemory-api/src/host/usage.rs) |
real tokens + charged_amount_usd |
tree::summarise::summarise (tinymemory-core/src/tree/summarise.rs:169-188) |
populates SummaryOutput.input_tokens |
HostSummariser::summarise (tinymemory-core/src/engine/summariser.rs:52) |
returns .output, drops usage |
bucket_seal.rs:274 / document_seal.rs:196 |
call .summarise(), not summarise_with_usage() |
| audit row |
zeros |
summarise_with_usage() has exactly one production caller: tinycortex/memory/sync/rebuild.rs:208, the engine rebuild path. RealCostAccumulator exists and is tested — wired only to rebuild, never to source sync. Embeddings have no token accounting anywhere.
L4 — the host cost tracker is never called from memory. cost::record_provider_usage and cost::record_embedding_usage exist, persist durably, and feed openhuman.cost_get_dashboard. Neither has a single caller from modules/memory_host.rs or from inference/embeddings/.
L4c — tinyinference never surfaces embedding usage. EmbeddingModel::embed returns Result<Vec<Vec<f32>>> (tinyinference/src/embeddings/types.rs:51) and no file under tinyinference/src/embeddings/ parses a usage object. Every real embedding request routes here via TinyAgentsEmbeddingProvider (inference/embeddings/provider_trait.rs:86), including the managed cloud path, which is only a decorator over it (cloud_adapter.rs:153). The serde_json::Value decode in inference/embeddings/factory.rs belongs to DimensionAgnosticOpenAiProbe, the Test-connection probe — not production embedding traffic. So provider-reported embedding tokens cannot be obtained host-side.
L4b — UI drops the fields that could already be non-zero. SyncAuditPanel.tsx:216,297 sums and renders estimated_cost_usd only. Rust has SyncAuditEntry::effective_cost_usd() (actual_charged_usd.unwrap_or(estimated) + composio_cost_usd) and summarise_month already uses it; the panel does not. The TS interface at app/src/utils/tauriCommands/memoryTree.ts:1077-1099 does not even declare composio_cost_usd, actual_charged_usd or composio_actions_called, though the wire carries all three.
Why it regressed
#3110 is closed as completed against a code path that no longer exists.
Field proof
~/.openhuman-staging/users/69dc8c37935c9a8c539b43f2/workspace/state/memory_sync_runs.jsonl — 10 rows, plus 2 in memory_tree/sync_audit.jsonl = the 12 on screen.
{"timestamp":"2026-09-15T13:13:01Z","source_kind":"composio","scope":"github:ca_leUcIgnTw0DJ",
"items_fetched":50,"batches":0,"input_tokens":0,"output_tokens":0,"estimated_cost_usd":0.0,
"composio_actions_called":0,"composio_cost_usd":0.0,"actual_charged_usd":null,
"duration_ms":35511,"success":true}
Every row identical in the cost half.
Solution
Measured numbers only — no item-count estimates. Both inference seams already execute in the openhuman host process: the module installs BusEmbeddingHost / BusChatHost (tinymemory-module/src/lib.rs:136-143) which call back over tinybus into EmbeddingCallbacks::embed and ChatCallbacks::complete in crates/openhuman-core/src/modules/memory_host.rs. ModelResponse already carries usage. So the host can meter 100% of memory inference spend with no contract change.
The chat half is reachable host-side; the embedding half is not, and embeddings are the dominant memory spend (a sync that embeds 506 chunks may summarise with the no-LLM fallback_summary and spend nothing on chat at all). So the embedding half sets the PR count.
Repo nesting that fixes the order: openhuman → vendor/tinyagents → vendor/tinyinference, with openhuman's root Cargo.toml patching tinyinference to vendor/tinyagents/vendor/tinyinference/crates/tinyinference. A tinyinference change therefore reaches openhuman only through a tinyagents pointer bump.
PR 1 — tinyinference
Add EmbeddingModel::embed_with_usage returning vectors alongside an optional provider-reported usage, with a default implementation delegating to embed and reporting None, so no existing implementor breaks. Parse the usage object in the OpenAI-compatible, Voyage and Cohere models (all three return it); Ollama and Noop keep the default. Release.
PR 2 — tinyagents
Bump the vendor/tinyinference submodule pointer to that release. No code.
PR 3 — openhuman
- Bump
vendor/tinyagents, taking the new tinyinference through the existing patch.
inference/embeddings/provider_trait.rs — call embed_with_usage and, when usage is reported, record_embedding_usage(provider, model, tokens, dims, vectors). embed()'s public signature is unchanged; a provider that reports nothing records nothing.
modules/memory_host.rs — ChatCallbacks::complete maps ModelResponse.usage → UsageInfo and calls record_provider_usage. build_token_usage already skips all-zero payloads and sets CostSource::ProviderCharged when the backend echoes a charge.
- No new tag needed:
ai.tinyhumans.tinymemory.EmbeddingHost is memory-only, and on the chat side role already discriminates ("summarization" is special-cased at memory_host.rs:129).
- UI — Sync History header shows measured memory spend for the window; per-row
Tokens/Cost render — with a tooltip rather than a fabricated $0.0000. TS type gains the three missing fields plus an effectiveCostUsd() mirroring the Rust accessor.
Outcome: real memory spend visible in Sync History and in the existing cost dashboard, and memory spend starts counting toward budget accounting. Per-row attribution still blank.
Separable now: steps 3–5 of PR 3 depend on nothing upstream and can ship first as an openhuman-only PR. That stops the $0.0000 claim immediately and meters summarisation, but on a profile using fallback_summary it will record nothing until the embedding half lands.
PR 4 — tinymemory (only if per-row attribution is wanted)
EmbeddingHost::embed and ChatHost::complete gain a trailing scope: Option<String>, appended at tail (MINOR — wire slots are append-only); bucket_seal.rs / document_seal.rs pass the source scope they already hold. No accounting in this PR — the host does the metering. Release.
PR 5 — openhuman
Re-pin rides inside this PR (precedent: #6269 pinned tinyconnectors v0.10.0 in the fix PR). TokenUsage gains a scope field (host-internal type, no contract); sync_audit_log_rpc joins the ledger by scope and fills the per-row columns.
Why per-row needs PR 4+5
The bus calls carry role, provider, model — no source id. Sealing is a queued async job (mem_tree_jobs), batched across sources and running after the run's row is written, so time-window attribution across concurrent syncs would be a guess rather than a measurement.
Acceptance criteria
Related
Summary
Brain › Sync › Sync History renders every run with
0tokens and$0.0000, and the header totals read0 in / 0 out · $0.0000 total. The reader and the UI are correct — no writer anywhere puts a non-zero token or cost figure on a source-sync audit row. Underneath it is a wider gap: memory inference spend never reaches the host cost tracker at all, so it is also missing fromopenhuman.cost_get_dashboardand from budget accounting.Problem
What happened — 12 sync runs listed,
Itemscorrect (50, 50, 0, 50…),Tokens0andCost$0.0000on every row.What was expected — real provider-reported token counts and cost for runs that embedded 506 chunks against cloud
voyage-3-large.Impact
AgentActivityPanel's monthly memory cost (openhuman.memory_sources_monthly_cost_summary) is$0.00for the same reason.CostTracker::check_budget, whichagent/tinyagents/host/budget_gate.rsuses for budget refusal and accounting. Memory inference is currently unbudgeted and unreported.Steps to reproduce
Items: 50,Tokens: 0,Cost: $0.0000.$0.00.Environment — dev build on
main@2475ea342, tinymemory pin v1.16.0, macOS. Staging profile~/.openhuman-staging/users/69dc8c37935c9a8c539b43f2.Root cause
Four independent layers. All four must be addressed or the symptom survives.
L1 — host writer hardcodes zeros.
crates/openhuman-core/src/memory/sources/run_history.rs:96-100—HostRun::into_entrywritesbatches: 0, input_tokens: 0, output_tokens: 0, estimated_cost_usd: 0.0, andHostRunhas no token fields to carry. Covers every Sync click, Apply-all and Composio run (10 of the 12 rows).L2 — driver writer hardcodes the same. tinymemory
crates/tinymemory-core/src/sources/sync.rs:182-184and:228-230, the periodic folder/GitHub/RSS/web loop (the other 2 rows). Zero since the file was created at tinymemory v1.5.0.L3 — usage is produced, then dropped mid-chain.
UsageInfo(tinymemory-api/src/host/usage.rs)charged_amount_usdtree::summarise::summarise(tinymemory-core/src/tree/summarise.rs:169-188)SummaryOutput.input_tokensHostSummariser::summarise(tinymemory-core/src/engine/summariser.rs:52).output, drops usagebucket_seal.rs:274/document_seal.rs:196.summarise(), notsummarise_with_usage()summarise_with_usage()has exactly one production caller:tinycortex/memory/sync/rebuild.rs:208, the engine rebuild path.RealCostAccumulatorexists and is tested — wired only to rebuild, never to source sync. Embeddings have no token accounting anywhere.L4 — the host cost tracker is never called from memory.
cost::record_provider_usageandcost::record_embedding_usageexist, persist durably, and feedopenhuman.cost_get_dashboard. Neither has a single caller frommodules/memory_host.rsor frominference/embeddings/.L4c — tinyinference never surfaces embedding usage.
EmbeddingModel::embedreturnsResult<Vec<Vec<f32>>>(tinyinference/src/embeddings/types.rs:51) and no file undertinyinference/src/embeddings/parses ausageobject. Every real embedding request routes here viaTinyAgentsEmbeddingProvider(inference/embeddings/provider_trait.rs:86), including the managed cloud path, which is only a decorator over it (cloud_adapter.rs:153). Theserde_json::Valuedecode ininference/embeddings/factory.rsbelongs toDimensionAgnosticOpenAiProbe, the Test-connection probe — not production embedding traffic. So provider-reported embedding tokens cannot be obtained host-side.L4b — UI drops the fields that could already be non-zero.
SyncAuditPanel.tsx:216,297sums and rendersestimated_cost_usdonly. Rust hasSyncAuditEntry::effective_cost_usd()(actual_charged_usd.unwrap_or(estimated) + composio_cost_usd) andsummarise_monthalready uses it; the panel does not. The TS interface atapp/src/utils/tauriCommands/memoryTree.ts:1077-1099does not even declarecomposio_cost_usd,actual_charged_usdorcomposio_actions_called, though the wire carries all three.Why it regressed
memory_sync/sources/{github,rebuild}.rs— the legacy paths.96c370314(refactor(memory): complete TinyCortex engine migration #4794, TinyCortex migration) guttedgithub.rs861→few lines andrebuild.rs836→few; the accounting moved into tinycortex, where onlyrebuildkept it.4ce2a8c7c(refactor(memory): retire legacy sync compatibility paths #5246, "retire legacy sync compatibility paths") deleted the remainder.memory_sources/sync.rs— the Sources path that survived and is now the only path — always wrote zeros, even pre-migration. It never received feat(memory): surface actual LLM usage/cost from inference API in sync audit #3110's work.#3110 is closed as completed against a code path that no longer exists.
Field proof
~/.openhuman-staging/users/69dc8c37935c9a8c539b43f2/workspace/state/memory_sync_runs.jsonl— 10 rows, plus 2 inmemory_tree/sync_audit.jsonl= the 12 on screen.{"timestamp":"2026-09-15T13:13:01Z","source_kind":"composio","scope":"github:ca_leUcIgnTw0DJ", "items_fetched":50,"batches":0,"input_tokens":0,"output_tokens":0,"estimated_cost_usd":0.0, "composio_actions_called":0,"composio_cost_usd":0.0,"actual_charged_usd":null, "duration_ms":35511,"success":true}Every row identical in the cost half.
Solution
Measured numbers only — no item-count estimates. Both inference seams already execute in the openhuman host process: the module installs
BusEmbeddingHost/BusChatHost(tinymemory-module/src/lib.rs:136-143) which call back over tinybus intoEmbeddingCallbacks::embedandChatCallbacks::completeincrates/openhuman-core/src/modules/memory_host.rs.ModelResponsealready carriesusage. So the host can meter 100% of memory inference spend with no contract change.The chat half is reachable host-side; the embedding half is not, and embeddings are the dominant memory spend (a sync that embeds 506 chunks may summarise with the no-LLM
fallback_summaryand spend nothing on chat at all). So the embedding half sets the PR count.Repo nesting that fixes the order: openhuman →
vendor/tinyagents→vendor/tinyinference, with openhuman's rootCargo.tomlpatchingtinyinferencetovendor/tinyagents/vendor/tinyinference/crates/tinyinference. A tinyinference change therefore reaches openhuman only through a tinyagents pointer bump.PR 1 — tinyinference
Add
EmbeddingModel::embed_with_usagereturning vectors alongside an optional provider-reported usage, with a default implementation delegating toembedand reportingNone, so no existing implementor breaks. Parse theusageobject in the OpenAI-compatible, Voyage and Cohere models (all three return it); Ollama and Noop keep the default. Release.PR 2 — tinyagents
Bump the
vendor/tinyinferencesubmodule pointer to that release. No code.PR 3 — openhuman
vendor/tinyagents, taking the new tinyinference through the existing patch.inference/embeddings/provider_trait.rs— callembed_with_usageand, when usage is reported,record_embedding_usage(provider, model, tokens, dims, vectors).embed()'s public signature is unchanged; a provider that reports nothing records nothing.modules/memory_host.rs—ChatCallbacks::completemapsModelResponse.usage→UsageInfoand callsrecord_provider_usage.build_token_usagealready skips all-zero payloads and setsCostSource::ProviderChargedwhen the backend echoes a charge.ai.tinyhumans.tinymemory.EmbeddingHostis memory-only, and on the chat siderolealready discriminates ("summarization"is special-cased atmemory_host.rs:129).Tokens/Costrender—with a tooltip rather than a fabricated$0.0000. TS type gains the three missing fields plus aneffectiveCostUsd()mirroring the Rust accessor.Outcome: real memory spend visible in Sync History and in the existing cost dashboard, and memory spend starts counting toward budget accounting. Per-row attribution still blank.
Separable now: steps 3–5 of PR 3 depend on nothing upstream and can ship first as an openhuman-only PR. That stops the
$0.0000claim immediately and meters summarisation, but on a profile usingfallback_summaryit will record nothing until the embedding half lands.PR 4 — tinymemory (only if per-row attribution is wanted)
EmbeddingHost::embedandChatHost::completegain a trailingscope: Option<String>, appended at tail (MINOR — wire slots are append-only);bucket_seal.rs/document_seal.rspass the source scope they already hold. No accounting in this PR — the host does the metering. Release.PR 5 — openhuman
Re-pin rides inside this PR (precedent: #6269 pinned tinyconnectors v0.10.0 in the fix PR).
TokenUsagegains a scope field (host-internal type, no contract);sync_audit_log_rpcjoins the ledger by scope and fills the per-row columns.Why per-row needs PR 4+5
The bus calls carry
role,provider,model— no source id. Sealing is a queued async job (mem_tree_jobs), batched across sources and running after the run's row is written, so time-window attribution across concurrent syncs would be a guess rather than a measurement.Acceptance criteria
$0.0000for a run that cost money.usagerecords nothing and logs at debug; no item-count estimate is presented as a measurement.openhuman.cost_get_dashboard.CostTracker, socheck_budgetaccounts for it.SyncAuditPanelrendersactual_charged_usd + composio_cost_usdwhen a row carries them, and—(not$0.0000) when a row genuinely has no cost recorded.serde(default).Related
96c370314), refactor(memory): retire legacy sync compatibility paths #5246 (4ce2a8c7c) — the migrations that removed the accounting.tree_ingest_failurespartial verdict on the same row shape.