Local knowledge storage
src/knowledge/ provides local storage, deterministic qualification, confirmed model writes, explicit personal pack installation and a resumable HTTP sync engine. The terminal supports local browsing, confirmed edits and explicit sync selection; the runtime reuses the selected MCP authorization and loads frozen session preferences. The desktop provides knowledge, memory and workflow pages with confirmed local actions and sharing through the selected connection. Existing MEMORY.md files and skills are unchanged. The remaining boundaries are listed in Known issues.
Protocol mirror
Circle syncs with any server that implements knowledge-sync v1, the neutral protocol specified in circle-hub’s docs/protocols/knowledge-sync-v1.md. circle-hub is the reference implementation, and the specification, schemas and vectors are published in its repository. The versioned JSON schemas in src/data/knowledge-sync/v1/ come from those personal-sync@1 contracts, together with the knowledge-sync-discovery@1 schema. The adjacent provenance.json records the source repository, committed source 0fc93fcb213676922a40734c5dc3a4495f9f7662 and exact file digests. All mirror inputs come from that commit, including contracts/src/spaces.ts; no working-tree observation is used. The original sources and local adaptations are separately pinned. Circle imports no server code, database or business runtime.
The frozen shared vectors are copied without modification into tests/fixtures/knowledge-sync/v1/. They pin exact canonical UTF-8 bytes, IDs and route definitions. The additive capability vectors (the discovery document and error answers with detail, the purge confirmation and an unknown code) are copied into tests/fixtures/knowledge-sync/v1-capabilities/. The vectors describe merge and transport expectations; they do not establish live merge, authentication, purge or server atomicity.
# Check the pinned mirror without an upstream checkout.
npx tsx scripts/verify-knowledge-sync.ts
# Also compare every recorded input with an explicit, read-only upstream checkout.
npx tsx scripts/verify-knowledge-sync.ts --upstream ../circle-hub-sync
Run the explicit upstream check at the start of every implementation stage. Refresh only from a committed upstream state and record its commit; a changing, dirty checkout is never a refresh source, and git archive <commit> gives a clean one. A missing or changed explicit upstream fails. Offline mode reports zero upstream comparisons; it cannot detect unseen upstream changes. Do not regenerate expected bytes from the encoder to accept a mismatch. A protocol update must review the contract, refinements, original vectors and provenance together.
Ajv validates the mirrored schemas without coercion, defaults or removal of unknown fields. Requests and objects stay closed. Server answers are checked as the protocol’s section 13 asks of a client: members Circle does not know are ignored, and an error or verification code it does not know is accepted rather than refused (validateAnswer). Additional checks preserve the source contract’s Unicode, UTF-16 field lengths, canonical base64, sorted sets, unique batches, registered request errors, pack paths/dependencies and checked-purge rules, which JSON Schema alone does not express. This adds no dependency.
Editable strings remove one leading BOM, change CRLF/CR to LF and normalize to NFC. Other whitespace stays significant. Object keys sort by UTF-16 code units; array order changes only when callers explicitly construct a contract-declared set. Duplicate JSON members, normalized key collisions, malformed UTF-8/Unicode, non-JSON values and altered or noncanonical object bytes are refused.
Source and quotation bytes stay in base64 byte objects and are not normalized. The source’s raw_sha256 describes its original assembled bytes; the object ID describes a typed canonical envelope. Neither ID is a server src:, tree: or rec: reference or a grant of access.
Separate local store
new KnowledgeStore(home) requires an explicit runtime home. It creates knowledge/knowledge.sqlite and private knowledge/objects/ inside that home, and never opens circle.sqlite. Terminal and desktop homes therefore have separate clients. The persistent random client ID survives reopening; item IDs supplied in revisions survive renames and moves.
The database uses its own application ID, append-only migration sequence and user_version. A newer version or an unrelated populated database is refused before changing its version or enabling WAL. Files have mode 0600 and directories 0700 on Unix. SQLite runs with foreign keys, WAL and FULL synchronous writes. No existing memory or skills directory is migrated or uploaded. Migration 4 appends local revision timestamps for actual local commits; old or remote revisions without supplied timestamps stay unknown. Hierarchy locations display space names with ` › ` separators; root rows omit the location, and Inspectors show the space and recorded update time.
put / putBatch validate and stage immutable objects. The writer holds a database write transaction while publishing a same-directory temporary file, syncing it and atomically renaming it, then registering the object. A local batch is bounded to 64 objects and 8 MiB of encoded bytes; source uploads need multiple batches. Sharing violations during rename get at most four retries; other errors fail immediately. This is filesystem recovery, not a retry of a failed sync operation.
Staged objects do not enter search or current browsing. commit(snapshot, expectedHead) validates the complete reachable object graph and uses a local compare-and-exchange. Revisions, current projection, FTS and current head change in one transaction. Omission cannot delete an existing item: deletion requires its own revision and parents. Old object bytes remain readable. Conflicting heads have no selected search body, and a resolution must descend from every previous head.
Graph checks cover typed dependencies, revision identity, explicit deletion parents, duplicate item IDs across pages, source size/hash, directory cycles and NFC path collisions. A directory inventory has one current root; canonical empty and pack-only inventories need no invented directory. Source verification streams chunks; the graph keeps at most 8 MiB of object bodies cached. Graph traversal is bounded to 100,000 objects and depth 512. The local whole-source limit is 32 MiB, independently of the protocol’s 8 MiB envelope and 1 MiB chunk limits and any smaller server deployment limit. These are resource bounds, not verification of a statement’s meaning.
Reopening under the database write lock removes only owned temporary files and unregistered orphan object files. A crash after publishing a file cannot make it registered or searchable. A crash before the snapshot transaction commits leaves the old head and old index together. Registered staged objects and committed history are retained. Symlinks cannot redirect object publication or reads to an outside file.
Search
Search text uses the reference server’s deterministic tokenizer: NFKC, lowercase, adjacent Unicode letter/number/underscore pairs within each word, and isolated single characters. It uses neither Intl.Segmenter nor FTS trigram. Two-character Chinese words are searchable. This normalization affects only the index and query, never evidence bytes.
FTS5 receives hex-encoded tokens, not user query syntax. Searches include statements, data and UTF-8 quotations, with folder, entry type and status filters. Limits are 4,096 query bytes, 100 results per page, 1 MiB per-entry token budget and 64 MiB of encoded terms per current projection. Continuations bind the query, filters and current snapshot. A changed snapshot or query refuses the old cursor. rebuildSearch() derives the current projection again from the immutable snapshot.
Local qualification and model tools
text@1, json@1 and markdown@1 are portable adaptations of the pinned circle-hub parsers, decoding and canonical selector codecs under Apache-2.0. Their upstream test cases and generic Markdown fixture are pinned alongside the implementation; the server-only sandbox input-serialization test is outside this client adaptation. credential-scan@3 scans decoded and encoded views using the same portable scanner. Its HTML character-reference data retains its BSD notice. No other parser is installed or executed by claiming a name in a manifest.
circle-local-verification@1 checks current conflict-free sources, exact located quotations and qualified local entry dependencies. Whitespace, case and Unicode form remain significant; empty/whitespace-only quotations cannot qualify. Raw originals and quotations retain their bytes, including BOM/CRLF and decomposed Unicode. Parser output follows that declared parser’s decoding rules. Unsupported parsers and native server references without an authorized resolver remain incomplete, not qualified. Missing/failed reads are incomplete; a located quote mismatch is recorded separately. A verification operation inspects at most 64 MiB, caches parsed documents within that work budget, allows 100,000 nodes per parse and bounds entry-dependency depth to 100. A limit produces an incomplete observation, never success.
Observations live in a migration-2 side table keyed by snapshot, object and local rule version; they record scanner/parser versions, positions and reasons. Immutable revisions contain no local qualification flag. A new snapshot rechecks current entries and rebuilds the search status in the same transaction. The local observation proves deterministic quotation/path conditions only, never statement meaning or server share authority. The server must independently reverify.
The runtime exposes knowledge_read as a bounded address/schema discovery tool. Content requires explicit snapshot and object IDs. object: "schema" returns a paginated copy of the pinned write format without opening a store. With the local context on (the default, which includes the local knowledge store), knowledge_search, knowledge_write, memory_write and knowledge_share are listed from the first request too. A runtime with localContext: false lists only the reader until a knowledge tool has been called in the conversation; words in the prompt never decide it. The curated empty-workspace prompt fixture models that isolated case and still enforces its unchanged 7,000-token ceiling; a default greeting carries the four extra schemas, about 500 tokens more. Runtime tests check both listings. Runtime configurations with extra local/integration tools have their own additional payload. A trusted host task lock refuses search and personal writes and permits only its explicit snapshot/object allowlist. This stage provides the host port; it does not infer a server lock from model text.
knowledge_write proposes at most 64 typed revision/byte objects and 8 MiB against expected_head; Circle derives inventory pages and the snapshot. Revision parents must continue every current head, so moves, deletions and conflict resolutions cannot silently discard a branch. memory_write creates, edits, enables, disables or deletes a stable memory identity. Memory Markdown is limited to 24,000 UTF-8 bytes per document; title, path and Markdown together are limited to 48,000 bytes and 20 enabled documents. Disabled current documents use no context budget. A conflict reserves one document and the largest candidate, using the protocol’s 8,000-byte path reservation when its directory is unresolved. Existing MEMORY.md is neither imported nor changed, and eligible structured memories load into the next session or after an explicit refresh.
Both writes require a person to approve the exact canonical object preview. Auto/accept-edits modes, saved rules, hook allows and model confirm: true cannot authorize them. A headless call cannot write. Rejection/cancellation leaves bodies, current head, search and history unchanged. After approval the database write lock rechecks the expected snapshot, then publishes the validated objects, observations, projection and head together. A changed version requires a fresh proposal and confirmation.
Credentials and bounds are checked before publication, including complete originals whose credentials could cross chunk boundaries. Rejected proposals are reduced to safe reasons before durable tool-call history/events and opaque provider replay; detector positions contain no matched text. No rejected body is put in objects, the index or temporary files. Binary or unsupported scan views have no clean conclusion and are refused by these write tools. Low-level KnowledgeStore.put/commit remain trusted storage/transport primitives, not model tools or a credential-acceptance API.
Personal packs
A pack_manifest revision is portable manifest data only. It carries no code. installPersonalPack is an explicit host service requiring the current snapshot, selected local folder, existing workspace/user skill scope and a person approval callback. It compares pack.json to the revision, validates allowed/required paths, aggregate size (32 MiB), exact file sizes/hashes and credential scans, refuses symlinks and same-name skills, then installs private files under knowledge/packs/<manifest-object>. It never copies a folder into the automatic skills/ root. The original selected files are unchanged. The desktop imports an explicitly selected compatible folder containing pack.json, then offers a separate file installation. Arbitrary standalone skill folders are not converted automatically. Code transfer through an authorized MCP pack entry remains separate.
Installation and enable/disable are separately confirmed and recheck the current snapshot. Only current, conflict-free, enabled installations with all files intact appear in the model skill catalog. Existing built-in/user/project precedence is preserved. Disabled, changed or missing files disappear from the next model request; loading a stale catalog item also refuses. Workflow and skill kinds remain distinct metadata; local installation is not server validation of a locked workflow version.
hooks.json is not an allowed file in the pinned pack protocol and is never copied from an existing skill folder. Personal pack discovery ignores a manually added hooks sidecar too; installation never fabricates a hook approval hash. Hook import/enable has no new entry point in this stage. Existing standalone skills and their hook behavior remain compatible, with no automatic migration, scan or upload.
Team sharing uses only the tools the selected server declares for the operations entry-share and memory-share in its discovery document (mcp.capabilities: a tool, the argument that carries the request and members it fixes). A server without a discovery document gets the shapes earlier servers used: upload_file with share_entry or memory_share, after a get_nodes read of the native entry’s verified status; with a document the share call itself is where the server rechecks the entry. Sharing requires the exact current accepted personal projection, a locally verified entry, or a successful memory-share check (check_only: true). After person approval, the host rechecks the local revision, account binding and remote state before dispatching. No guessed native reference is accepted. The model tool always requires a single person approval; auto mode, hook allows and remembered approvals do not bypass it. An unavailable connection/mapping, or a server that declares no such operation, refuses sharing. In local-only mode, “分享到团队…” is disabled with a switch-to-sync tooltip and a settings entry. There is no temporary sync or new server endpoint. Every refusal the server explains (an error’s message and fix, a failed memory-share check, a share that was not merged) is shown verbatim as refused: {message} {fix}, whatever its code; a refusal without an explanation stays the generic unavailable failure. The desktop share dialog shows the message and fix verbatim under “服务器拒绝了这次分享” / “the server refused this share”, and an unexplained refusal of the share as “服务器拒绝了这次分享,没有给出原因” / “the server refused this share and gave no reason”. A verified local upload can still be refused by circle-hub with SHARE_UPLOAD_UNSIGNED: quotation verification is not an upload signature. Stage-6 acceptance records that refusal, qualified synthetic team-source sharing, round trips with an isolated circle-hub and local-package results.
Resumable personal sync engine
KnowledgeSync(home, transport, options) opens only the supplied home. Its transport is SyncHttp bound to one explicitly selected server and a host-supplied Bearer provider. It finds the sync base in the server’s discovery document, read once per connection without credentials from /.well-known/knowledge-sync on the MCP entry’s origin (src/server_resolution.ts). The document must name that origin as its issuer and keep its sync and local-tools bases on it, and it must offer v1; unknown members are ignored. Only a server that answers 404 or 410 has no document: Circle then uses the fixed v1 paths under the entry’s base (the entry URL without /mcp, so a path prefix is kept) and /knowledge status says so. Any other discovery failure is an error, retried as offline, never a silent fallback. HTTP uses only the 12 v1 route templates relative to that base, omits cookies, refuses redirects, accepts HTTPS or loopback HTTP and bounds streamed JSON responses to 16 MiB. The request identity in a receipt binds the template as the frozen http.json writes it (/api/sync/v1/…), wherever the server serves it. No model tool exposes this transport. No access/refresh token is written to the local database, pending operation, result cache or status observer. The runtime supplies those ports from the explicitly selected MCP connection, never from model arguments.
Migration 3 appends a private sync journal and a singleton lease table. The journal binds one active server/account identity, epoch, common snapshot, frozen session/cursor/descriptors, immutable pending request/digest, receipts and successful commit time. A different host binding cannot dispatch an old queue. Explicit host confirmation is required to initialize from retained local bytes after an observed epoch change or local mode; a token refresh must not change that stable identity. The client does not infer an account from a device name or credential value.
The lease uses SQLite’s atomic write lock and a random fencing token, canonicalizing aliases through the store’s real home. Its heartbeat is 5 seconds, expiry 30 seconds, and no PID is used for takeover. Every request checks the live fence before dispatch and after response; local journal/head/projection changes check it inside the same write transaction. A delayed or expired holder cannot renew, release its successor or advance the local baseline. Already dispatched remote work may still complete after cancellation or ownership loss; only a recovered receipt establishes its result.
The driver registers the client, opens a frozen session, persists inventory pagination, imports only complete batches with matching IDs/kinds/sizes, then merges by stable identity and revision ancestry. The common base cannot be omitted. Different identities form a union; identical or ancestor heads collapse; concurrent revisions, delete-versus-edit and directory position races retain all heads. Same-name directories and deleted ancestors with surviving children suppress dependent paths/search and produce structural conflicts without deleting content. Duplicate current entry content at the same path also remains conflicted, matching the reference server’s projection rule. Malformed graphs, missing identities, cycles and multiple independent roots are refused rather than renamed or discarded; the immutable local and remote branches remain stored. The engine never uses a model or timestamp to choose content. Existing confirmed revision writes resolve a conflict only by continuing every head.
Transfers obey both protocol limits and smaller server object/count/decoded-batch limits; base64 wire expansion is accounted for by the server’s advertised decoded-batch budget. An operation ID, exact body and JCS request identity are durable before dispatch. Lost upload/commit/mode replies query /operations first; only an unavailable receipt replays the same operation and exact request. A changed payload cannot reuse its key. Expired sessions are recreated using retained local revisions and accepted objects. A CAS mismatch downloads the new frozen state and makes a new merged candidate/operation, never just changes expected_state on the old request.
Only a validated commit receipt advances the common base, successful timestamp, locally recomputed verification, current head and index. All commit result pages (verification, projections and conflicts) remain private journal observations, distinct from local verification; pages must keep the same receipt and state and have a bounded 64 MiB aggregate. A local edit made during HTTP remains a pending descendant. No remote qualification flag upgrades a local search result. Uploads, partial downloads, quota failures and lost replies do not advance the timestamp.
start/notify coalesce work with a 250 ms lower bound and 15-second idle polling. Network/service errors back off up to five minutes; 429 respects the larger server minimum. 401 pauses as login required without an automatic browser/login loop. A server’s detail on an error (capability error-detail) is kept as the last refusal and shown to the person as it came, until the next success; an error code Circle does not know is a generic failure of that request, retried with back-off rather than paused as invalid. A server that advertises purge-confirmation answers a purge with 403 SYNC_PURGE_CONFIRMATION_REQUIRED and a confirmation URL: Circle keeps the exact pending request, shows the URL and its expiry, asks again every 15 seconds and completes when the account holder has confirmed in the server’s console; an expired request is dropped, never performed. Local/purging mode or a changed epoch stops the old queue; retained local bytes are unchanged. cancel stops scheduling and aborts local in-flight waits, retaining uncertain requests for receipt recovery. abortTransfer is an explicit, receipted remote session abort and retains local revisions. close cancels and releases resources. Mode changes call /mode/check, require a person callback on the exact impact preview, recheck the expected state and persist the operation before /mode; decline/stale/canceled previews cannot silently purge. Terminal person confirmation and global selection persistence are implemented; desktop settings use the same checked host operations.
Hermetic tests use a loopback protocol peer and isolated homes; they establish client state transitions and stored/network effects. tests/knowledge-neutral-server.test.ts syncs two clients with a minimal server written from the specification alone (tests/fixtures/knowledge-sync/neutral-server.ts, Node built-ins only), which serves a different base, adds unknown members, explains refusals and requires purge confirmation. Separate stage-6 receipts cover real password/OAuth exchange, conflict preservation, checked purge/resume, mapped sharing and revoked-token re-login against an archived isolated circle-hub with its private test PostgreSQL/vault (scripts/knowledge-server-acceptance.ts). Development and packaged Electron use the synthetic peer and have their own receipts. None establishes deployed availability or business correctness.
Selected MCP authorization
KnowledgeConnection accepts an existing HTTP/SSE MCP connection selected through /knowledge connect. Before that selection it sends no sync request to any configured service. Global knowledge_sync stores only its profile name and opaque binding; projects cannot replace it. Selecting a different account requires fresh person approval before retained local revisions can upload. Direct Bearer headers reuse their existing secret/environment references. Replacing a direct credential changes the binding.
OAuth uses the existing private mcp-auth.json, official MCP SDK discovery/PKCE and token refresh. Ordinary MCP traffic and sync share one refresh in flight per grant, with a private cross-process lock and a second token read after acquiring it. Rotating and nonrotating refresh tokens are both supported. A refresh keeps the opaque grant identity; an explicit login creates another. The background path uses cached discovery only: no registration, browser, discovery fallback or login loop. Failed refresh pauses as login required. /knowledge login explicitly forgets the old grant and invokes the original MCP login; select the new account again afterward. A same-grant token repaired externally can resume with /knowledge sync.
Logout, credential replacement, disabled/changed configuration and disconnect stop the old client; before accepting responses the host checks the binding again. Loaded team preferences are removed from the current session on authorization loss/disconnect. An already-dispatched remote operation may still finish; the durable journal recovers its receipt. Only the local host sees credentials, never the knowledge database, model prompt, renderer or sync status. Refresh has a 30-second deadline and last-waiter cancellation; canceling one shared waiter leaves other callers alive.
Session preferences
Preparation reads team memory through the read-only tool the server declares for team-memory-read, its argument and fixed members (circle-hub: get_nodes, memory, scope: "team"; the same shape without a discovery document), with for_session: true. A server that declares no team memory loads none, without a warning. It freezes list and document versions across all pages. Only enabled, auto-load-allowed team documents are accepted; shared personal spaces are excluded. A changed/stale/malformed page discards the complete team snapshot with an unavailable notice, while eligible local personal preferences can still load. Local disabled/deleted/conflicted memories and unresolved directory paths are excluded. Credentials are scanned after chunks are assembled, including titles/paths and the selected connection’s current credentials.
The actual model request orders team preferences, personal preferences, then existing instruction/memory files. Structured preferences are explicitly scoped below user/project instructions, escaped and described as preferences rather than evidence, task inputs or executable instructions. Team plus personal content is capped at 48,000 UTF-8 bytes including escaped text, titles, paths, wrappers and omission/status text, further reduced to fit the complete model request’s input budget. Truncation respects UTF-8 and escape boundaries and reports omitted bytes/documents. Existing files retain their 24,000-byte per-file cap and normal context handling; they are not migrated or uploaded. The empty prompt/tool ceiling is unchanged.
A loaded snapshot does not change mid-session when local or remote revisions change. A new session, reload or idle /memory refresh prepares another snapshot; authorization loss removes team content immediately. localContext: false loads none and starts no sync. Headless runtimes without integrations load eligible local preferences only. /memory loaded displays the actual bounded structured context, while /memory team browses the frozen eligible team documents.
Terminal commands and current limits
/knowledge uses existing pickers and status surfaces. It supports list/search/read, a bounded explicit UTF-8 source import, revision history, conflict candidate selection, statement edits, directory/source rename and moves, deletion, connection selection, explicit sync/login and checked mode changes. Every content write, conflict resolution, connection selection and destructive mode change asks the person, including in auto mode; a changed local head or mode impact requires a fresh preview. The card is named for the action (write memory, write knowledge, share to team, connect sync server, change sync mode, sign in again) and shows the change in words: action, scope, path, title and line count, the text added, never an object ID, head or reference. /knowledge mode synced|local keeps the wire value hub for synced. /knowledge status gives the condition, the server’s connection name, the last successful sync, the item and conflict counts and the next step. The footer lamp is yellow only while Circle talks to the server, cyan when the sync waits for the person (a conflict, a login, a purge to confirm), red after a failure and unlit while changes wait to be sent. A runtime sync change that the person has to act on leaves one transcript notice. Transcript rows of knowledge tools show titles and paths and summarize results in words. Imported sources preserve original bytes and have no invented verified statement. Import reads one available workspace file of at most 1 MiB and never imports MEMORY.md automatically; malformed UTF-8, credentials and outside paths are refused.
/memory personal, write <title> | <text>, edit <number> <text>, enable, disable and delete manage structured preferences with the same person-only confirmation/CAS checks. Original bare /memory, add [account] and remove remain file operations. Rejected proposed text is not added to terminal input history. Team/loaded/refresh inspect the frozen snapshot and its real bounded prompt. The terminal remains English on both palettes. The desktop /knowledge and structured /memory commands open their pages; the existing file-memory commands keep their file guidance.
Desktop pages and boundary
The existing WorkbenchShell contains personal knowledge lists and workflow tables, a source inspector and returnable details below 1,180 px. Knowledge supports folders, bounded search/filtering, original source viewing, confirmed edits/moves/deletion and explicit conflict resolution. Recent revision history is bounded and reports omitted older changes. When no sync binding is saved, knowledge and memory show only the personal scope, including when opened through a team/shared URL. The sidebar says “未连接同步服务器” / “sync server not connected” and opens sync settings; once bound it names the connection, for example “已连接 · Synthetic workspace”. Circle’s desktop shows no Hub wording. With a binding saved, the existing team/shared scopes remain available and read-only, including in local-only mode. Shared knowledge requires a query; browsing shared memory does not add it to the model context. Memory labels reflect the actual frozen, possibly truncated session content rather than assuming every enabled document loaded.
Settings → sync selects an existing MCP profile, shows authoritative mode, last success and connection/conflict/login status, and supports explicit login and sync. Switching to local-only first displays /mode/check bytes, directory grants, pending shares (including unknown) and independent team copies. Confirmation rechecks the full impact. Purging stops old transfer work; bounded state-only polling reports completion honestly while retaining local objects.
The sandboxed renderer sends validated operations through the existing preload/runtime bridge. It receives opaque item/revision/source capabilities and expiring, session-owned single-use confirmation tickets, never raw object addresses, SQL, native selection paths or tokens. File/folder pickers run in main; their paths remain on the private worker channel and pass the existing protected-path policy. Current credentials are refused from returned content without rewriting original text. Full editable content is fetched before editing, independently of bounded detail previews. Account/session/head changes invalidate prepared work. Local pages remain usable without a selected connection; locked/view-only tasks cannot mutate them.
Original files and skills are never migrated automatically. Workflow manifests sync as data, while each device explicitly selects and installs exact matching files; hooks are excluded. Compatibility import currently requires pack.json; it is not a converter for arbitrary skill folders. Source import is explicitly selected valid UTF-8 of at most 1 MiB, preserving BOM, CRLF and whitespace. Native picker automation in development and packaged fixtures selects only generated scratch originals; it does not establish manual OS-picker acceptance.