Privacy, cost & limitations
September 1, 2026 Ā· View on GitHub
For anyone deciding whether to trust Tacit with their conversations. Everything
below is the actual behaviour of the code on main; where it has a rough edge it says so.
What stays on your machine
Everything Tacit keeps lives under ~/.dsh/storages/tacit/ (or
$DSH_HOME/storages/tacit/). Files are written atomically (write a temp file, then
rename) and never truncated in place.
| File | Holds |
|---|---|
profile.json | mistake patterns with trust counters, directives (max 8 global + 4 per workspace, each with the normalised absolute workspace path it is scoped to, if any, and its provenance: updatedAt, version, the ids of the reports it was distilled from, the ledger run id of that distillation, evaluatedAt and approvedAt), when a session was last seen in each of those workspaces, the last 6 retired or removed directives so the distiller is not told to propose them again (their own sentence, ids and counters; no prompt text), style rules (max 6), the last 10 š verdicts, counters |
reports/<conversation>/<turn>.json | one analysis report per analyzed turn (problems, improved prompt, explanation, a 200-char excerpt of the prompt, your correction if any, the absolute workspace directory of the conversation) |
config.patch.json | the settings you changed in the UI |
auto.json | today's date and how many automatic analyses were spent |
usage/<YYYY-MM-DD>.json | one day's usage ledger: runs (one bootstrap batch, auto-analysis, ⨠Improve, distillation, ...), each with its attempts (op, timing, model, provider, token counts, finish reason/code, the attempt's status, priced USD) ā never prompt or response text |
usage/summary.json | rolling lifetime / by-type / by-model / by-provider / by-trigger / by-day usage totals, kept alongside the day files so nothing has to re-scan them; every bucket also carries failedCalls and failedUsd, the count and the cost of calls that were billed but whose attempt failed |
The turn digests themselves are not here ā they live in the harness's own session projection store, next to the session.
What is sent to the model
Every call goes through the harness's model service with your configured key ā
Tacit never reads, stores or forwards the key. Only the model id is allowlisted
(deepseek-v4-flash / deepseek-v4-pro); the provider route is the one your
session already uses, so a proxy or self-hosted route configured in the harness
is honoured. Nothing is sent anywhere else.
| Call | What goes out | Clipped to |
|---|---|---|
| Analysis | previous turn's prompt + answer; the analyzed prompt; step/tool/error/retry/token counters; the first 25 tool calls (name + arguments); the final answer; your correction | 600 + 600 Ā· 4000 Ā· ā Ā· 400 per call Ā· 3000 Ā· 1000 chars |
| Directive distillation | your top 12 patterns (kind + one example); the last 5 "prompt ā correction" pairs; your style rules; the current directives | 200 Ā· 120 + 200 Ā· 300 Ā· 220 chars each |
| Style-rule distillation | your last 3 š reasons, verbatim | 300 chars each |
| ⨠Improve | your draft; the last 2 turns' prompts and answers; style rules; last 3 š reasons; trusted patterns | draft as typed (⤠100 000) Ā· 600 + 600 each Ā· 300 Ā· 300 Ā· 200 |
| Pre-send context (opt-in) | your draft; enabled directives; top 6 patterns; the last 2 turns | 1500 Ā· 220 Ā· 160 Ā· 600 + 600 |
Everything above that comes from a past turn ā prompts, answers, tool arguments,
the recent conversation ā is read from the turn digest, and the digest is masked
at capture: a credential-shaped string is already [redacted:...] before any of
these calls is assembled. The one text that is not is the draft you are typing
right now, which ⨠Improve and pre-send context send as typed.
Never sent: tool results or file contents (only whether a tool errored), API keys, session ids, anything outside the clips above.
Guarantees
- No telemetry. Tacit sends no telemetry or analytics of any kind. The only
network traffic it produces is the model call through the harness's own model
service. A weekly
log-auditworkflow scans this repository's own CI logs for credentials, personal paths and addresses. - Never reads or stores API keys. Calls use
ctx.llm.stream, the harness's own service. - Credential-shaped strings are masked at capture. API keys, tokens, JWTs,
private-key blocks and
key=valuesecrets in a prompt, a tool argument or an answer are replaced with[redacted:...]in the turn digest, so nothing downstream (reports, corrections, any model call) sees them; the list of shapes is inlib/redact.js. - Directives never change policy. The steering section states that it does not alter tool permissions, approvals or the sandbox, and a directive that talks about those is rejected, whether distilled or typed.
- Visible steering. The exact injected text is in Settings ā Tacit ā Exact text
injected, and
steerAgent: falseremoves it entirely. - Append-only pre-send.
enrichPrompts(off by default) appends a separate, labelled, plugin-sourced message; your words are never rewritten. - Same-origin only. The harness web server has no origin policy, so Tacit's
routes check
Sec-Fetch-Site,OriginandContent-Typethemselves: a web page you happen to visit cannot plant a directive or spend your budget through127.0.0.1. (A request with none of those headers ācurl, the smoke test ā passes by design.) - Three deletion paths, all restricted to Tacit's own files. Clear all
analysis reports removes
reports/*/<turn>.jsonfiles (and the emptied folders). Day files older thancostHistoryDaysare deleted automatically, at most once per calendar day. Clear usage history resets the whole ledger at once. All three only ever unlink files matching Tacit's own naming (reports/*/<n>.jsonorusage/<YYYY-MM-DD>.json); theusage/directory,summary.json(rewritten fresh, not unlinked), the profile and the config are never touched by any of them. - Bounded everything. Every text sent to the model is clipped (table above); directives ⤠220 chars, the steering section ⤠1400 chars, 8 directives, 12 patterns.
Cost
Every Tacit model call is metered from the model's own usage block (uncached
input, output, cache-read, cache-write and reasoning tokens) and folded into a
content-free ledger under usage/. Reasoning tokens are always a subset of
output tokens, never a separate quantity, so they are never billed twice.
Runs and attempts. The ledger groups calls into runs ā one bootstrap
batch, one auto/manual/batch analysis, one ⨠Improve, one directive
distillation, one style-rule distillation, one pre-send enrichment ā and each
underlying model call inside a run is an attempt. A run carries an id, its
type, a trigger, start/end times, a derived status ā success when no
attempt failed (an unmetered attempt still counts as not failed), partial
when some attempts failed, failed when every attempt failed or there are no
attempts, running while open ā the session id and turn, the workspace
label (never the path), the model/provider and its list of attempts. An
attempt carries an id, the op (analysis, analysis-repair,
directive-distillation, style-distillation, improve, improve-repair,
enrichment), its timing, model, provider, reasoning effort, finish
reason/code, a status (ok / failed / unmetered), the session id and
turn, the five raw token buckets, and the priced result (null when nothing
matched). None of it is ever a prompt, a response, a tool argument or an API
key.
The tiles and the run list count differently on purpose. A tile is summed from the per-day totals, which count each attempt on the day it was billed. A run row lives in the day file of the day the run started. So a run that begins before midnight and finishes after it puts tokens into two days' tiles, and it is listed under today as long as any of its attempts happened today. That is the reading that makes the today tile and the today list agree on the same work.
Pricing sources and resolution order. Each attempt is priced once, at the call's own start time, in this order:
dsh-cost-meter's own model rates for that model, when the call went through an official DeepSeek route ā tiered off-peak/peak usingdsh-cost-meter's own peak windows / effective-at / peak-enabled flag when it supplies them, with Tacit's own Beijing-weekend rule always applied on top (see Tiers below) ādsh-cost-meterhas no such rule of its own, which is exactly why the two plugins' figures can differ on a weekend (Known limitations).dsh-cost-meter's provider rates for that provider + model, when the above did not match ā a flat rate, no tier, so a proxy or custom route can still be priced when the meter knows its rate card.- The bundled DeepSeek V4 list price (dated
2026-08-22), for an official route and an allowlisted model, when neither of the above applies. - Otherwise: no price at all ā see Unpriced calls below.
dsh-cost-meter's rates always win when they match; the bundled table is the
fallback, never a ceiling. The plugin is entirely optional and duck-typed:
Tacit never blocks a model call on it, and a getState() call that hangs,
throws or returns nothing usable falls back to the bundled table within 5
seconds, remembering why (shown in the Pricing card).
Formula. For one attempt:
cost = (uncachedInput Ā· cacheMissRate
+ (cacheRead + cacheWrite) Ā· cacheHitRate
+ output Ā· outputRate) / 1,000,000
in USD per 1M tokens.
Tiers. Peak hours are 01:00ā04:00 and 06:00ā10:00 UTC; every other UTC hour is off-peak. Since 2026-08-22T16:00 UTC, Beijing-calendar weekends (Saturday/Sunday at UTC+8) are always off-peak regardless of the hour. The tier is decided once, at the call's start time, so a call that straddles a tier boundary is still priced consistently.
Unpriced calls. A call on a route Tacit can't match to any price table ā
a proxy or custom provider neither the bundled table nor dsh-cost-meter
know ā has no computed price; it's counted separately as an unpriced call
(shown in the Usage card) rather than assumed free. A call that never
received a usage block at all (the adapter reported none) is unmetered,
which is never shown as $0.00.
Every call is also made with low reasoning effort and a tool schema (no prose
to parse), and every one is tagged with the session id, so a cost plugin such
as dsh-cost-meter can additionally show the real spend next to the
conversation.
| Call | When | Max output tokens | Counts against the daily cap | Tagged with session | Est. cost, deepseek-v4-flash* |
|---|---|---|---|---|---|
| Analysis ā auto / correction | messy turn or correction | 3000 | yes (30/day) | yes | ā $0.001 off-peak Ā· $0.002ā0.003 peak |
Analysis ā good (learnFromGood) | a clean turn right after a messy one | 3000 | yes (same 30/day) | yes | ā $0.001ā0.002 (shorter answer than a diagnosis) |
| Analysis ā manual / bootstrap | you click | 3000 | no | yes | same; bootstrap (20 + 1 distillation) ā $0.02ā0.05 |
| Repair retry | analysis or Improve returned unparseable JSON (rare) | same as the call | no | yes | doubles that one call |
| Directive distillation | every 3 analyses | 1500 | no | yes | ā $0.0005ā0.001 |
| Style-rule distillation | every 3 š with a reason | 1000 | no | yes | < $0.001 |
| ⨠Improve | you click | 1500 | no | yes | ā $0.001ā0.002 (its system prompt is a stable, cache-hit prefix) |
| Pre-send context (opt-in) | every send with an 8ā1500-char draft | 1000 | no | yes | < $0.001 each, but on every send |
* Computed from DeepSeek's list prices (per 1M tokens) with a typical analysis of ~2500 input and ~800 output tokens (reasoning tokens count as output; the other calls are smaller):
| Model | Cache hit | Input (cache miss) | Output | |
|---|---|---|---|---|
deepseek-v4-flash | $0.007 off-peak Ā· $0.014 peak | $0.22 off-peak Ā· $0.44 peak | $0.66 off-peak Ā· $1.32 peak | default |
deepseek-v4-pro | $0.022 off-peak Ā· $0.044 peak | $0.66 off-peak Ā· $1.32 peak | $1.98 off-peak Ā· $3.96 peak | 3Ć flash |
Cache-hit input is ~30Ć cheaper ($0.007ā0.044), and Tacit's system prompts are stable, so real spend is often below the estimates. Peak/off-peak hours and current prices: DeepSeek pricing.
What the Usage card breaks down. Spend on failed or repair calls is shown as
one figure. A repair retry that itself failed is both a failed call and a repair
call, and the figure counts it once, not twice. Spend by route separates an
official DeepSeek route from a proxy or self-hosted one. Spend by trigger
separates auto, correction, good, manual and bootstrap runs. The input and
output token split is on the tiles as well as the run rows. Today's automatic
analyses are shown against the daily cap (autoDailyBudget), beside today's
spend.
Guards: automatic analyses are capped per local calendar day (autoDailyBudget);
each turn is analyzed at most once automatically; bare continuations are skipped
without a call; turns that finished before the plugin started are ignored.
Known limitations
Honest list ā these are deliberate behaviours, not hidden surprises:
- Steering is frozen per conversation. A verdict, an edit or a new directive applies to conversations started afterwards; the running one keeps what it started with. Removing a directive or switching one off is the exception, and re-freezes every open conversation at its next system-prompt assembly.
- Trials are a trend check, not an A/B test. A candidate is judged only on turns
from conversations whose frozen steering text actually contained it; conversations
started before it existed (or before a restart) contribute nothing, so a trial can
take more than
directiveTrialTurnsturns of wall-clock time to conclude. - Corrections are detected by a heuristic. A turn counts as corrected when your next message matches the correction markers (How it works, §3); a correction phrased differently is missed, and a conversation's last turn is never counted as corrected.
- "Messy" means two slightly different things. Auto-analysis also counts 15+ steps as messy; the trend and the trial verdict do not (long-but-successful work is never held against a directive).
- Only automatic analyses are capped. Manual, bootstrap, ⨠Improve, both
distillations and pre-send context run outside
autoDailyBudget. - The trend only sees loaded conversations, and only turns within
maxKeptTurns; it needs 40 finished turns to show. - Bootstrap runs a small worker pool (
bootstrapConcurrency, 1ā4, default- and ignores the daily cap.
- The distiller sees workspace names (the last path segment, e.g.
dsh-tacit), never full paths; full paths stay in the local reports and profile. - A weekly digest is not implemented yet.
- Bundled prices are a snapshot, dated
PRICES_AS_OF(currently2026-08-22). A DeepSeek price change is only reflected once this package updates, or viadsh-cost-meter. dsh-cost-meterhas no weekend rule. Its price table carries no Beijing-weekend off-peak override, so on a weekend a bundled-table figure and adsh-cost-meterfigure for the same call can differ.- The measured bootstrap estimate uses only day files on disk. It reads
the priced attempts already written to disk, not an in-flight run's own live
counters. A run that has finished is already there ā
endRunwrites its day file synchronously ā so only a run still in flight lags: its attempts do not move the estimate until the debounced flush (250 ms) has written them.
ā How it works Ā· Next: Configuration