跳到內容

Reversible context retrieval: source and revocation contract

The native tool loop may replace a long, post-redaction tool result with a CCR preview only after the original is committed to its local SQLite store. The model receives an opaque retrieval ID, while duduclaw_ccr_retrieve checks the caller’s exact tenant, agent, session, and CCR ACL before returning a bounded UTF-8 fragment. The ID is a lookup handle, not authority. In later turns, duduclaw_ccr_find can search the same active scope with a 3–128-byte query and return at most five valid handles, without placing saved content into the stable prompt prefix. Exact phrase matches rank first. If the phrase is absent, a bounded lexical fallback uses up to four distinct ASCII words or eight terms when Han characters appear, including overlapping Han-character bigrams. It requires at least two matched terms, keeps ASCII word boundaries, then ranks by matched-term count, closest term distance, and recency. Each hit says whether the full phrase matched and gives a byte offset for targeted retrieval; a lexical hit should be retrieved by ID and offset because the full query need not exist verbatim. The model explicitly retrieves a selected fragment. Search filters current trusted source routes before the 1,000-candidate cap and orders candidates by exact phrase, ASCII-boundary-aware matched-term count, and recency; it then rechecks scope, expiry, revocation, integrity, and the route allowlist before returning a handle. Proximity is ranked after the cap, so very large stores can still omit a closer older hit with the same term count. This is lexical ranking, not semantic similarity or task-quality validation.

duduclaw_ccr_find is model-visible and each call scans the caller’s whole scope, so one native tool loop may call it at most eight times; the ninth call returns an explicit error result telling the model to answer from what it already retrieved, and the refusal is counted in ToolLoopTelemetry.ccr_find_rate_limited, which is persisted as a ccr_loop_telemetry column and aggregated per tenant by the CCR dashboard projection. The column arrives with CCR store schema version 2; a database written by an earlier build gains it through the store’s own versioned migration the next time the store opens, and the read-only dashboard projection reports zero for that counter until then rather than failing the snapshot. The scan itself is indexed on the full (tenant, agent, session, source ACL) scope tuple, computes each candidate’s lexical score once instead of rebuilding it for ranking, and lowercases each in-scope original once rather than once per search term — a bounded temp-store copy (capped by the store’s 64 MiB original budget) traded for a per-row cost that would otherwise grow with the attacker-shaped term count.

The native find response includes source_validation_limited. When true, the local causal-source validation budget was reached; an empty hits array then means no validated hit was returned, not that every matching source was checked. The bounded lookup does not disclose unchecked bound handles.

The channel reply also ranks its latest twenty saved session references against the current user turn and displays at most five opaque IDs in the uncached prompt tail. Each candidate passes exact caller scope, current route allowlist, expiry, revocation, integrity, and current bound-source checks before scoring. The scorer reads the saved original only inside CCR, returns a number, and checks the source again afterward; the gateway revalidates every selected ID before displaying it. It uses at most twelve distinct task terms from the first 4,096 characters, ASCII word boundaries, Han bigrams, and a 160-byte co-occurrence window. A single shared word from a multi-term task scores zero. Score ties and turns with no usable terms keep newest-first order. No original or matching excerpt is inserted into the prompt, and retrieval still rechecks access. This deterministic lexical order cannot resolve semantic ambiguity or prove a handle is useful; real-model task-quality and cache measurements remain pending.

The shared CcrRuntime denies all source routes by default; callers must configure trusted server/tool routes before storage, discovery, or retrieval. The gateway enables CCR only when an explicit channel-user task-local, non-ambient session, and nonempty [ccr] allowed_sources list are present. Its scope includes a SHA-256 tag of that user identity, so two users sharing an agent/session cannot retrieve one another’s originals. A placeholder identity is not a user identity: a principal that is blank, or exactly unknown, anonymous, or system (trimmed, ASCII case-insensitive), yields no scope at all, so CCR stays off for that turn. Channel adapters that cannot read a sender id from the inbound payload therefore disable CCR for it rather than pooling every unidentified sender of that channel into one shared retrieval scope; the adapter’s own placeholder value remains in its logs, session keys, and audit fields. The tool loop obtains the MCP server from the trusted registry and stores only exact server/tool routes listed in configuration. On each valid configuration load, removed routes are scrubbed and their saved call IDs tombstoned in that exact scope, including when the allowlist becomes empty or CCR is disabled. Re-adding a route cannot revive an older handle or its call ID. Unlisted tools still return their full result without a CCR marker. This still does not infer an upstream artifact ACL or version from arbitrary tool output; that provenance must be supplied by the tool connector before source-level authorization is complete.

The local operator can invalidate this scope with duduclaw ccr-revoke-scope --db ... --tenant local --agent ... --session ... --principal ...; the command derives the same tag as the gateway. The older exact --source-acl form remains available for existing entries. Revocation removes saved originals and blocks reinsertion in the targeted scope.

Valid JSON and JSON Lines use a lossless compact form only when that meaningfully reduces size. Already-dense structured results pass through intact; CCR does not store them or replace exact fields with a lossy preview. Object/array-shaped input rejected by the current JSON value parser also passes through intact: valid numeric lexemes such as 1e400 can exceed its floating-point range, so unvalidated structured input must not fall through to lossy truncation. “Object/array-shaped” means bracket-delimited at both ends ({…} or […] after trimming), which every JSON document satisfies; text that merely opens with a bracket — a log tail beginning with a bracketed timestamp, prose beginning with a Markdown link — is ordinary free-form content and stays previewable. Long free-form logs and search text can still use bounded previews with diagnostics and query matches. For search-like tools, query lines are selected across the full original by exact phrase first, then bounded ASCII words or Han-character bigrams matched within one line, ordered by term count and proximity; only a bounded set enters the preview. This can retain a relevant middle hit when the full query is absent verbatim. Existing diagnostic lines remain prioritized. Long Markdown previews also retain a bounded set of section headings and their first content line, including sections outside the head and tail fragments. Regular CSV/TSV tables with wholly numeric measurement columns retain up to six rows outside Tukey-style 1.5-IQR fences (or differing from a zero-IQR common value), with source line numbers; quoted or irregular tables fall back to the generic preview.

duduclaw ccr-eval runs a fixed synthetic suite covering lossless, lossy, and bypass delivery. It reports byte counts before and after a targeted retrieval only when required content is absent from delivery, required-value recovery, cross-turn handle-search attempts, hits, and exact-versus-lexical match flags, cross-scope and expiry refusals for both search and retrieval, and local preview/search p95. It also applies the gateway’s local CJK token heuristic to raw, delivered, and retrieved text, and adds a minimum estimate for the query and opaque-ID text used in a needed find/retrieve cycle. This is a lower bound on control overhead. The native tool loop separately exposes ToolLoopOutcome.telemetry and a structured ccr_loop_metrics event with provider-reported usage summed across all rounds, usage-reporting round count, compressed original/delivery bytes, search/retrieval hit and miss counts, retrieved bytes, and elapsed milliseconds. Completed CCR-enabled native loops also schedule a best-effort write of these counters, their tenant ID, and observation time to a content-free SQLite table capped at the newest 10,000 rows across all tenants. No query, result text, handle, principal, or source ACL is stored in that table. The admin dashboard aggregates only retained rows for one exact tenant and computes nearest-rank p95 loop duration. Provider errors before loop completion, telemetry write failures, a full 16-task telemetry queue, or process shutdown before a scheduled write completes can leave no persisted observation. The measured loop duration stops before telemetry persistence, and the response never waits on that SQLite write. Missing usage rounds, provider-specific prices, task-success labels, controlled raw/lossless/lossy comparisons, and real observations still prevent a net-cost-per-success claim.

duduclaw ccr-compare --observations fixtures/ccr/paired-task-synthetic.json accepts a JSON object with cost_source (synthetic, user_supplied_billing, or replay-only unbilled_replay) and observations. Each observation supplies a shared task_id, one arm (raw, lossless, lossy, ccr), success, a billed-cost value (null for an unbilled replay), a serialized ToolLoopTelemetry, and four lowercase SHA-256 condition digests: exact task input, source set, model settings, and authorization scope. Every task must have exactly one observation per arm, with all four condition digests identical. The report divides total cost across all attempts by successful tasks and also gives success rate, p95 loop latency, output/cache usage, and retrieval misses. The command checks pair completeness and counter consistency but cannot attest caller-computed digests, billing, or success labels. The bundled fixture validates the calculation only. A live comparison still needs controlled execution with identical conditions across arms, plus independently checked outcomes and invoices.

duduclaw ccr-compare-replay --evidence fixtures/ccr/native-replay-synthetic.json recomputes a narrower recorded exact-value check from a local four-arm evidence bundle. Each arm supplies its exact task input, source text, model settings JSON, principal-derived CCR scope and trusted route claim, per-round ChatResponse records, and native-loop telemetry. The command rejects mismatched task/source/model/scope/route digests, anonymous or malformed principal ACLs, missing source calls, changed provider/model claims between rounds, inconsistent round counts or token sums, CCR find/retrieve attempt counts that disagree with the recorded tool calls, and incomplete arm sets. It derives success from whether the last completed response contains the source-grounded exact oracle; it does not accept a supplied success flag. Output includes only hashed task IDs, condition digests, numeric telemetry, the derived outcome, and the comparison report; caller task labels are hashed before any report or error is formed. The evidence origin is explicitly synthetic or recorded_claim. The command replays scoring from supplied records; it neither reruns a model nor independently verifies the provider, upstream ACL, elapsed time, or task semantics. The Dashboard’s tenant-level CCR aggregates have no task identifier or non-CCR arms and cannot be used as paired task observations. Replay has no independently verified invoice path, so every cost and cost-per-success field is null. The plain ccr-compare input still accepts complete synthetic or user-supplied billing values, which remain unverified claims.

The admin CCR Dashboard can submit the same recorded evidence bundle to POST /api/ccr/replay?tenant_id=... for the same scorer used by the CLI. The endpoint requires every arm to declare the selected exact tenant, computes the four-arm comparison without storing the uploaded source, prompt, response, or task labels, and returns only hashed labels, digests, numeric telemetry, and scoring limitations. The upload is an explicit operator action; the page clears an older result when the selected tenant or bundle changes. The dashboard’s store-health aggregates remain separate from this caller-supplied replay and cannot prove the recorded provider, ACL, latency, invoice, or task oracle was genuine.

The controlled local provider runner records a digest of the source tool result delivered in each arm. Replay checks that raw and CCR tool results match the common source; the CCR middleware compression counter separately identifies tasks whose model-visible result changed. The scorer returns a per-arm count of changed tasks when all delivery digests are present, and the Dashboard shows it next to success. A bypassed lossless or lossy treatment therefore remains visible as zero changed tasks instead of being mistaken for a different intervention. Older recordings without these digests show unavailable. These digests and counters are recorded claims, not independent delivery attestation.

duduclaw ccr-compare-synthetic runs the twelve ccr-eval fixtures through the native tool loop under four delivery policies. The task prompt gives a lookup clue while the required full value remains a test oracle. The lossy arm uses the same preview transform but replaces the CCR retrieval marker with an explicit unavailable notice. A deterministic mock provider first reads a source, then checks whether the required value is visible; with CCR enabled it can call the real scoped find and retrieve tools when the preview omits that value. The suite covers noncontiguous ASCII and Han-character query lines preserved in previews and buried values recovered after nonexact multi-term handle search followed by offset retrieval. The command emits all task observations and the paired report. Its usage is a local text-token estimate and its cost uses a fictional rate of one millicent per estimated input token plus five per estimated output token. This checks the routing, retrieval, observation, and comparison pipeline; it is not evidence of provider billing or real model task success.

The synthetic lossless arm calls the same validated JSON/JSONL whitespace compactor as the CCR preview. Its duplicate-key/numeric-lexeme fixture keeps both occurrences of a key and the exact 1.0e+02 spelling across raw, lossless, lossy, and CCR arms. Parsing into a JSON value and serializing would collapse that key and change the lexeme, so it is not used as a lossless baseline. The fixture validates key and numeric-lexeme fidelity and the comparison pipeline; it does not attest real provider quality.

Retrieval writes bounded local audit records for grants and refusals, including missing or empty IDs. Audit rows contain the caller scope, a digest of the requested ID, status, byte count, and time; they do not copy the query or retrieved text. The tool loop’s own tracing logs follow the same digest boundary: they record id_sha8, the first eight hex characters of the same SHA-256 digest the audit table stores, never the cleartext handle.

The gateway derives CCR source_acl from the agent, task-local session, and a SHA-256 tag of the channel user. This isolates callers; a trusted causal or internal Wiki verifier must separately identify an upstream artifact or page before the original is source-bound. A source-specific deletion must not infer a match from the CCR content hash, tool name, or tool-call ID. Tool results need an explicit trusted upstream artifact reference before artifact-level propagation can be claimed.

The separate history-budget trimmer has no trusted CCR source identity at its boundary. When it strips an old tool echo, its marker therefore says the original is unavailable. The active background session summarizer reads bounded structured turns, removes exact ## 約束 / ## Constraints and ## 受眾 / ## Audience sections before calling the utility model, and appends their original bytes to the stored recap. Protected-only turns do not trigger a model call. At the request boundary, an async fallback can summarize older unprotected turns only after deterministic compression fails. It retains the newest three turns and exact protected sections, rejects model output that forges a protected header, and sends the rewritten history only when the whole request fits its token budget. Utility failure or an oversize result leaves the original history in place. The legacy synchronous bisect_and_summarize function is a no-op outside the default pipeline.

For a session access change, the local owner can revoke its entire exact CCR scope:

終端機視窗
duduclaw ccr-revoke-scope \
--db ~/.duduclaw/ccr/ccr.db \
--tenant local --agent support --session s1 \
--principal channel-user-id

This deletes every stored original in the specified scope and records a tombstone that blocks later inserts. Retrieval also checks scope and source-call tombstones, so an older writer cannot make a revoked original readable again. The command is idempotent and prints the number of originals removed. It acts on a local owner-controlled database; tenant and scope flags select records and are not remote authentication credentials.

When CCR is enabled, the native tool loop checks exact-scope tool-call tombstones for every returned result, including short results, tool errors, and dispatch failures that would not be compressed. Repeated calls with the same scope/server/tool/call ID and identical post-redaction content reuse the committed handle. A changed result for an ID with an active handle atomically tombstones that call, removes its old handles, and withholds the new result even when it is short. The loop also withholds results when status checks fail. This is local call consistency, not upstream artifact provenance; that integration remains open.

The CCR store accepts put_bound with a connector-verified artifact ID, version, and ACL revision, separate from the caller’s source_acl. revoke_artifact_version atomically tombstones that tenant/source/version, scrubs its bound originals across sessions, and blocks old call IDs from being replayed as unbound results. Search and retrieval also reject a tombstoned version at read time. A new source version may be stored under a fresh call ID. The native tool loop accepts optional executor-provided ToolOutcome::source_artifact only after a second check against the post-redaction bytes and a third check after the CCR write; a changed source scrubs the new handle before a marker can be emitted. The default executor verification hook refuses claimed bindings. Generic MCP output, including _meta, never certifies provenance by itself, while explicitly allowed generic routes retain scoped, unbound CCR. An owner can invoke duduclaw ccr-revoke-artifact-version --db ... --tenant ... --connector ... --artifact ... --version ... for the exact source version. Local CausalStore changes now enter a content-free, same-transaction SQLite outbox keyed by tenant, causal connector, artifact ID, and old version. Triggers cover direct invalidation/erase, retention expiry, ACL/version/content changes, and deletion through other connections after trigger installation. The gateway starts a drain at startup and every minute, processing consecutive 128-row batches with cooperative yields and a short pause after each 32 full batches; a failed CCR tombstone leaves the event pending for retry. The native causal read gate denies changed sources during delivery lag. Public direct CcrStore::retrieve refuses every source-bound original because it has no application-owned current-source validator; an authorized CcrRuntime performs that validation before and after its internal store read. A concurrent return can still race the final source check. A migration-complete marker commits atomically with the one-time backfill of existing invalidated/expired sources, so a crash after table creation retries that backfill. Acknowledged outbox rows are removed after CCR commits its durable, idempotent tombstone, so the outbox retains only pending work. The admin CCR dashboard shows read-only exact-tenant memory.db projections: pending causal revocation notices, active delivery leases, and source versions still awaiting fenced revocation, each with count and oldest age only. An unavailable or legacy source database is unknown rather than zero; the source and CCR dashboard snapshots are not atomic together. Arbitrary external connector events and live upstream authorization remain open. The CCR gate additionally requires task-quality and net-cost evaluation against uncompressed controls.

For artifacts registered in the causal store, duduclaw decision-invalidate-source --db ... --causal-db ... --ccr-db ... --tenant ... --acl ... --artifact ... also revokes CCR originals whose trusted connector is causal, artifact ID matches, and immutable version matches the causal record. The CLI requires the instance CCR database path and creates the file if absent before the causal invalidation begins. Cross-database updates are sequential, so a failure after causal invalidation requires rerunning the same command; the causal record’s version remains readable for that retry.

The native MCP registry used by the direct API, local inference, operator fallback, and OpenAI-compatible paths has a narrow local causal-source verifier. An operator may declare [[ccr.verified_causal_routes]] with server and tool, and the same pair must appear in [[ccr.allowed_sources]] and resolve to that exact mounted MCP server. The read tool must take only {"artifact_id":"..."} and return the complete source content as its entire text result. The verifier ignores any model/MCP version or ACL claim, reads the active artifact independently from <home>/memory.db under the authenticated CCR tenant and principal-specific source ACL, compares every returned byte, and binds the immutable local version plus an ACL-identity digest. It caps the CCR handle expiry at the artifact retention deadline. A mismatched, missing, expired, cross-ACL, or invalidated source withholds the MCP result from the model and tombstones that exact tool-call ID; an upstream error body on a verified route is also withheld; if a source changes after the first attestation, the loop withholds the result and tombstones that verified artifact version across sessions before proceeding. A declared trusted route whose verifier cannot be registered withholds its MCP result and retires its old call handle. A declared trusted route remains fenced even if CCR is disabled or its runtime cannot be created; without a verifier, the tool returns a fixed error before upstream execution. A malformed trusted-route declaration that cannot identify its intended tool fences every mounted MCP route. When no trusted-source declaration is present, ordinary MCP routes remain unbound. This is a principal-ACL-only connector: Decision Lab artifacts with ACLs such as private do not match the CCR principal tag and are not wired through this route. The gateway’s native CCR runtime now rechecks a bound local causal artifact’s exact tenant/principal ACL, immutable version, ACL identity, active state, and saved content digest before and after retrieval, before including a find hit, and before displaying a historical reference. A missing causal database refuses native bound reads without permanently tombstoning them. After the store builds a chunk, the native runtime rechecks the exact saved entry and binding identity; a row deleted between the internal grant and that final check causes a refusal. Find validates at most 8 distinct bound identities within its 1,000-row candidate window and conservatively omits later bound candidates, including valid sources ranked below that cutoff. Generic unbound MCP entries retain scoped CCR reads. The gateway accepts source-bound causal and explicitly configured internal wiki_agent routes; other bound connectors fail closed. Production external source-change event ingress remains open. The local outbox proactively scrubs after delivery. Direct public CcrStore reads fail closed for bound originals even before that delivery; unbound entries retain their normal scoped read path. Causal CCR retrieval now acquires a durable exact-source delivery lease after the store read. Acquisition rechecks source bytes, version, ACL, retention, and a staged revocation fence under one SQLite writer transaction; a source mutation waits for the lease to drain. The native tool loop retains the guard through the provider request and suppresses the response if the source expires or is fenced during that request. Gateway native-loop callers now capture those guards into a guarded reply envelope; channel adapters keep it through their awaited final send and check before document, chunk, and fallback sends. Every such recheck runs on a blocking thread, never on the async reactor, because one guard chain opens several SQLite connections and wiki-backed sources also read files. A recheck that observes a lost lease also rolls the turn back through the same path a build-time refusal takes: the assistant message already persisted for that turn is revoked in session history and the turn’s conversation-distillation job is told it was never delivered. Adapters get that for free from the still_valid call they already make. A turn that used no CCR source publishes its delivery verdict at build time exactly as before. Legacy string-only reply callers receive a conservative refusal when a guard would otherwise be dropped, and that withheld turn is rolled back the same way. Wiki delivery takes cooperative per-directory shared file leases for its exact source recheck only, records the durable fence generations it observed, and rechecks those generations before the provider request and every guarded channel send instead of holding the lock across the turn; WikiStore, WikiTrustStore, and the direct MCP wiki_write path take the exclusive lock of the Wiki root they are changing, with a bounded wait, and a write that still loses reports a distinct busy condition. A local lease and an external platform’s receipt are not one atomic operation, so an already accepted remote send cannot be retracted. Every new bound entry carries a durable binding_required marker. The store transactionally backfills this marker for existing binding rows and marks future binding inserts with a trigger. After marker installation, a missing binding row cannot turn a bound entry into an unbound one: retrieval refuses it, find omits it, historical references are invalid, and replay of that call ID revokes the old handle. A binding deleted before migration cannot be inferred from a legacy unmarked row; that older database integrity must be assessed separately. Direct mutation of the marker itself remains outside the store API integrity model. The CCR store records a read grant before the native post-read source check, so aggregate grant bytes may include a later native refusal. Production external connector event ingress, generic connector reconciliation, and remote receipt coordination remain open.

The admin CCR dashboard reads GET /api/ccr/dashboard?tenant_id=... behind the instance-admin gate. It shows aggregate retained and currently eligible entry counts, expiry and tombstone counts, retrieval audit counts from a bounded global 10,000-row window, and retained native-loop telemetry from a separate global 10,000-row window. A legacy store without the telemetry table remains readable and reports those metrics unavailable; an empty table has zero retained observations and no p95. The read never creates or migrates a store. It does not return originals, principal IDs, or ACL strings. Compression bytes measure only initial result delivery, provider token totals can be incomplete, and loop p95 is the pre-persistence native loop interval rather than end-to-end response latency. No billed costs, task outcomes, or quality measurements are stored. The same page shows a read-only exact-tenant connector lifecycle projection: journal availability, pending, blocked, and retained completed event counts, oldest pending and latest completed age, and an allowlisted failure code. It exposes no connector identifier, external ID, artifact ID, source digest, or source text. Completed means the local cross-store cascade finished, not that a remote platform acknowledged delivery.

The gateway starts a bounded local connector lifecycle retry worker at boot and runs it every minute. A trusted gateway-local adapter can bind an immutable causal artifact to one authenticated source identity and generation, then stage ACL loss, deletion, quarantine, or version change together with a causal disclosure fence in one memory.db transaction. The journal retries the DecisionStore cascade after a delivery lease clears; terminal events erase source text, while version changes invalidate the old version. The built-in Decision Lab synthetic adapter is a producer for this bridge: its admin-only create path regenerates and compares the exact ticket/staffing fixture before binding the local artifact, and POST /api/decision/synthetic-pilot/lifecycle derives the event target from that fixture and its existing binding. The Decision Lab UI exposes that synthetic operation and links to the tenant-scoped CCR dashboard. This proves a local end-to-end producer, replay, and display path. No production upstream connector authenticates or emits real source events yet; out-of-band changes and remote delivery receipts remain outside this coverage.

Internal Wiki source binding and delivery lease status

Section titled “Internal Wiki source binding and delivery lease status”

The internal duduclaw/wiki_read tool can be source-bound when both [[ccr.allowed_sources]] and [[ccr.verified_wiki_routes]] name exactly that mounted route and a revision-backed wiki_trust.db already exists. A missing or legacy trust database refuses binding and later bound reads rather than treating absence as a reusable ACL revision. The verifier accepts only the caller agent’s own single page path, independently rereads the entire raw page and current Wiki trust state, compares every returned byte, and binds the page path, raw SHA-256 version, a durable per-page trust generation and database incarnation, the page file identity/change time, and the CCR scope revision. A changed, missing, quarantined, or unavailable page refuses later native retrieval and is omitted from find/history. The wiki_agent source takes cooperative shared delivery leases only for its read-and-verify window and releases them immediately; the durable fence generations observed in that window are recorded and rechecked before every guarded send, so a page or trust change during the turn refuses the send instead of blocking the writer. The fence is per directory — each agent Wiki root, the shared Wiki root, and the trust-database home own a separate lock and a separate durable epoch — so one agent’s delivery no longer fails another agent’s Wiki or trust write. The observed Wiki-root and trust-home epochs are both included in the ACL binding and advanced before cooperative writes, so same-byte rewrites still do not revive old handles. Cooperative writers (WikiStore, WikiTrustStore row writes, the direct wiki_write MCP path) now take the exclusive lock with a bounded wait rather than failing on first contention, and trust-row writes take the fence of the Wiki root that owns the row; batch writes take one guard per touched root in sorted order. A write that still cannot take the fence returns a distinct “busy” condition that callers retry or report, never an “invalid input”. The direct wiki_read MCP path now withholds pages quarantined by frontmatter or an existing live trust database; a missing trust database retains legacy ordinary reads, while verified CCR binding still refuses that source. Out-of-band file or SQLite writes that bypass the cooperative fence remain outside its atomicity guarantee. This is a local Wiki connector, not proof of external-service artifact ACLs or events.

For causal bindings, memory.db stores content-free delivery lease rows and staged revocation fences. Lease acquisition uses an immediate transaction to recheck exact tenant, ACL, artifact ID, version, stored-content digest, retention, and fence absence. An OS lock per lease proves whether a crashed process can be reclaimed; a missing lockfile with a surviving row fails closed. Update/delete triggers reject source mutation while a matching lease exists. Owner invalidation first commits a fence, then tombstones CCR and waits for leases to drain before changing source bytes; direct SQL changes still enter the causal outbox. An expired lease denies further delivery and delays physical expiry scrubbing until release. The tool loop holds leases for both a first verified causal MCP result and a later CCR retrieval through provider completion, rechecking before and after the provider call; a failed acquisition withholds that tool result. The guarded channel reply retains these leases through its awaited platform send. A provider request already in flight cannot be retracted, and remote receipt needs an acknowledgment protocol beyond this local lease.

The dashboard separately samples exact-tenant active lease count/oldest age and unfinished fenced source count/oldest age. A source database that is absent, legacy, or unavailable is shown as unknown rather than zero. A completed fence is excluded from the unfinished count, even if its durable tombstone remains. These counters include no content or source identifiers and do not establish end-to-end delivery latency.

Channel document delivery rechecks a guarded reply during file validation, after the awaited delivery gate, after creating an archive copy, and immediately before each upload. A detected revocation removes only archive copies created by that guarded delivery and suppresses later uploads and source-derived file notes. Markerless office-document sweeps use the same checks. A revocation during an already-started remote upload cannot retract that upload; an append-only, content-free artifact ledger row may also remain after its newly archived file is removed. Cooperative Wiki page and trust writes now wait for the guarded send lease to release by returning a retryable Busy result; an out-of-band write or an already-started remote upload remains outside that guarantee.

config.toml [ccr] builtin_sources (default true, 2026-09-29) folds DuDuClaw’s own high-output read-only MCP tools into allowed_sources alongside whatever [[ccr.allowed_sources]] entries the operator listed:

db_select · db_query · csv_read · xlsx_read · file_read
web_fetch_cached · web_extract
wiki_read
memory_search · memory_fetch_batch

Names are the exact registered tool names (web_fetch_cached, not web_fetch). Every entry is read-only and returns retrievable source content; a tool that mutates state, or that echoes the agent’s own summary back (grounding::SELF_ECHO_TOOL_NAMES), is excluded — CCR’s promise is “the exact original is still there”, which only means something for content that came from a source.

This changes what is routable, never whether CCR runs:

  • [ccr] enabled = false (still the default) ⇒ the allowlist is empty regardless, and ccr_runtime::for_agent returns None. Built-in routes cannot switch CCR on by themselves.
  • min_compress_bytes (4096) is unchanged, so a result under that size is byte-identical either way.
  • builtin_sources = false returns the operator’s list unchanged, byte for byte.
  • The existing removal / tombstone semantics are untouched: dropping a route still scrubs its handles, and re-adding it does not resurrect them.

The default stays enabled = false because the four-arm cost/quality comparison (duduclaw ccr-compare-run) has not been run against a real provider on this machine — see wiki/reports/ccr-real-measurement-2026-09-29.md for the measurement procedure, the pass criteria, and the honest record that it is unmeasured.