# Shared worktree index design **Status:** shared readers, committed-tree generations, private reconciliation and the opt-in repository daemon are implemented. **Managed v2 is recommended for new shared-daemon integrations** and provides atomic advancement, owner guards, reference-aware collection, allocations and storage-only maintenance. It requires an explicit `--shared-policy` file; see the [managed contract](#managed-client-contract) below. Shared **v1 is deprecated as of tgrep 1.2.1 but remains supported**: no-policy startup still selects its retain-all, fixed-pin contract and warns once on stderr. This patch does not remove v1, change its protocol/storage, or implicitly migrate or collect legacy data. No removal date or release is set. The earlier sections describe the architecture and legacy v1 behavior. Ordinary single-root CLI/server behavior remains the default and does not emit this deprecation warning. ## Goal and ownership An agent runtime may create many worktrees from the same repository. Rebuilding a complete trigram index for every session duplicates indexing work, disk storage, and memory. Instead, tgrep should share immutable base data and index only the differences belonging to each worktree. The boundary is: **tgrep owns search correctness; the agent runtime owns session lifecycle.** The main checkout is another worktree with its own overlay, not a mutable source of truth for every session. | Responsibility | Owner | | --- | --- | | Shared readers, base generations, overlay precedence, persistence | tgrep | | Git-difference discovery, watchers, reconciliation, safe scan fallback | tgrep | | Worktree-specific filtering, filename membership, and content caches | tgrep | | Starting/supervising the daemon and configuring resource budgets | Agent runtime | | Attaching/releasing worktrees as sessions start/stop | Agent runtime | | Supplying the intended starting revision and known-change hints | Agent runtime, validated by tgrep | Runtime notifications are an optimization, not the source of truth. External editors, Git commands, and build tools can change files independently. ## Overall architecture Run one repository-scoped daemon, identified by the canonical Git common directory, rather than by a remote URL or branch name. Linked worktrees share this identity; unrelated clones do not accidentally share mutable state. ```mermaid flowchart TB subgraph Runtime["agent runtime"] Manager["Session manager"] A["Agent session A"] B["Agent session B"] end subgraph Daemon["tgrep daemon - one per repository"] Router["Request router
Select view by worktree identity"] VA["Worktree A view
Pinned base B1 + private overlay A
Own visibility rules and content cache"] VB["Worktree B view
Pinned base B1 + private overlay B
Own visibility rules and content cache"] Base["SharedBase B1
One immutable IndexReader
Shared postings and path table"] Changes["Per-worktree change tracking
Git differences + filesystem watchers
Reconciliation"] Generations["Base generation manager
Build, publish, pin and reclaim"] end subgraph Disk["Filesystem"] FA["Worktree A files"] FB["Worktree B files"] Bases["Immutable base generations
Outside disposable worktrees"] Overlays["Separate overlay checkpoints
A and B: changes + tombstones only"] end Manager -. "start / attach / detach" .-> Router A -->|"search A"| Router B -->|"search B"| Router Router --> VA Router --> VB VA -->|"base candidates"| Base VB -->|"base candidates"| Base VA -->|"verify against A's contents"| FA VB -->|"verify against B's contents"| FB FA -. "changes" .-> Changes FB -. "changes" .-> Changes Changes -. "update A only" .-> VA Changes -. "update B only" .-> VB VA -. "save A delta" .-> Overlays VB -. "save B delta" .-> Overlays Router -. "request suitable base" .-> Generations Generations -->|"publish new generation"| Bases Bases -->|"open once and share"| Base ``` The daemon shares the actual `Arc`, including its loaded path table, not just the names of the memory-mapped files. Worktrees retain independent roots, overlays, visibility/membership state, watcher queues, and synchronization so a checkout in one worktree need not block another's searches. ## Base snapshots and worktree overlays A base describes an exact committed tree under a compatible indexing profile. Its implemented key includes repository identity, Git tree OID, indexing profile, and index-format version; retain the commit OID for diagnostics. The profile must account for content decoding, checkout transformations, and corpus coverage. Dirty and untracked files from the main checkout belong in its overlay, never in the shared base. Every worktree pins an exact base generation. Its overlay contains the differences between that base and the worktree's current searchable contents, not merely differences from its current `HEAD`. | Difference | Overlay representation | | --- | --- | | Added or modified file | Postings for the complete current file | | Deleted or newly ineligible base file | Tombstone hiding its base entry | | Renamed file | Old-path tombstone and new-path entry | | Eligible untracked file | Worktree-only entry | | File restored exactly to the base | Remove the override and expose the base | Use Git diffs to discover paths, not to index patch hunks. Whole-file indexing preserves matching across edit boundaries, context lines, and line numbers. Discovery must include committed branch divergence, staged changes, unstaged changes, and eligible untracked files. A commit inside a session does not make its differences from the pinned base disappear. ## Search flow ```mermaid flowchart LR Q["Search in worktree A"] --> BC["Find candidates in base B1"] Q --> OC["Find candidates in overlay A"] BC --> Hide["Exclude every base path
replaced or deleted by A"] Hide --> Union["Combine candidates"] OC --> Union Union --> Filter["Apply A's scope,
visibility and query filters"] Filter --> Verify["Run full matcher against
A's file contents"] Verify --> Results["Results for session A"] ``` This illustrates the existing `HybridIndex` precedence model: overlay entries shadow base paths even when their new content does not match the query. Candidate IDs and path resolution must use the same pinned reader snapshot, with overlay mutations synchronized for that operation. The base supplies candidates, not another checkout's contents. Final matching reads the requesting worktree or its correctly versioned cache. Cache entries must not be keyed only by relative path across worktrees. Sharing cached bytes by content identity is a later optimization requiring verified identity. For example, editing `auth.rs` in A hides that path's base postings only in A. Deleting `old.rs` in A hides it with a tombstone. B still sees its own versions. The same membership/visibility rules must apply to content searches and `--files`, including filename-only paths without searchable postings. ## Registration, readiness, and freshness The versioned daemon API implements these operations; the concrete wire contract is below: | Operation | Purpose | | --- | --- | | `attach(root, revision, profile, lease)` | Select/pin a base, capture changes and return a view identity, lease and readiness | | `search(root, view, query)` / `files(root, view, query)` | Route requests to the private ready view | | `refresh(root, view, lease, changed, full)` | Invalidate immediately; return the processed epoch after reconciliation | | `detach(root, view, lease)` | Release one lease; retire the view only after the final lease | Normal CLI invocations should discover the same view automatically. Releasing one session must not stop a daemon or worktree view still used by other clients. Canonicalize and validate roots and request paths; a worktree identity is not permission to read arbitrary paths. Protocol capability negotiation must distinguish this service from the existing single-root server. On attach, start capturing changes before establishing the initial delta. Reconcile Git/filesystem state, replay queued events, and publish a consistent base-plus-overlay view. If coverage is incomplete or change discovery fails, scan rather than presenting a partial index as complete. A known changed path can be verified by direct scanning while its replacement postings are built. Keep periodic reconciliation and watcher-overflow recovery. Merely draining a watcher queue does not prove that no filesystem notifications were missed. An acknowledged refresh can establish that specified edits have been processed; it is not a filesystem-wide snapshot guarantee. `--no-index` remains the escape hatch for reading current disk contents under normal traversal rules. Sharing removes repeated full-content indexing, not necessarily all repository-sized startup work. Git status, untracked-file discovery, and watcher registration may still inspect many paths. Measure these costs separately before introducing filesystem-monitor or registration optimizations. ## When the base changes **Never update a shared base in place.** Build and publish a new immutable generation when a requested starting revision needs one. Deduplicate concurrent build requests for the same compatible base. | Event | Action | | --- | --- | | First session at a revision | Build a compatible base if none exists | | Another session at the same tree/profile | Reuse the base and establish its overlay | | Agent edits files or commits on its branch | Update only that worktree's overlay | | New session starts from an updated `main` | Build or reuse a base for the new revision | | `main` advances without a new session | No required work; background prewarming is optional | ```mermaid flowchart LR subgraph Old["Existing sessions remain pinned"] B1["Base B1
Committed tree C1"] A1["Session A
Overlay relative to B1"] --> B1 BView["Session B
Overlay relative to B1"] --> B1 end Advance["main advances to C2
New session requests C2"] --> Build["Build B2
Reuse unchanged postings
Index changed content"] B1 -. "reuse" .-> Build subgraph New["New generation"] B2["Base B2
Committed tree C2"] C["Session C
Overlay relative to B2"] --> B2 end Build --> B2 ``` Building B2 should reuse compatible unchanged postings and extract only changed content. It can still require streaming/writing a new full index generation; avoiding extraction does not eliminate publication I/O. In default v1 mode, active sessions do not migrate automatically. A checkout or rebase can be represented by recomputing that worktree's overlay against its pinned base. An explicit base migration must instead recompute the delta against the new base and atomically publish the new base-and-overlay pair. Swapping only the reader would give the overlay the wrong meaning. Retain old generations until no active worktree, in-flight query, or retained restorable checkpoint needs them. A retention policy may evict idle checkpoints and release their pins; restoration must then rebuild/reconcile, not silently substitute another base. Defer removal of mapped generations on Windows. The legacy `GenerationManager` implements **retain-all**, not online GC: it never deletes or rewrites a published generation, even when its last `Arc` pin is dropped. This also protects escaped readers/views and restorable checkpoints across process restarts. There is no deletion API or automatic active-session migration in that manager. Reclaiming legacy storage offline requires stopping all users and discarding dependent checkpoints. The managed v2 adapter described below adds ownership-aware retention and resource budgets in a separate namespace; absence of a live in-process pin alone is not proof that a generation is deletable. ## Persistence and compatibility Store bases outside disposable worktree directories. Persist each worktree's delta separately, bound to its exact base and identity. Checkpointing a session must not materialize another complete index or merge its changes into the base. | Concern | Required behavior | | --- | --- | | Ignore rules and corpus coverage | Use worktree-specific membership. A shared tracked-path superset can avoid irreversible omissions; newly admitted files absent from the base still need discovery/indexing | | Hidden files and `--files` | Maintain worktree visibility and filename-only membership, not just content postings | | CRLF, encoding, LFS, smudge filters | Git blobs are not necessarily checkout bytes; reuse postings only with compatible content semantics, otherwise index affected files or scan | | Sparse checkout, symlinks, submodules, case behavior | Respect materialized paths and existing traversal semantics, not just the committed tree | | Incompatible query options or partial coverage | Fall back to the existing scan behavior rather than omit eligible files | | Missing daemon/checkpoint/base | Use a validated reconciled view or scan; never silently answer from the base alone | | Restart or interrupted publication | Validate the generation/checkpoint pairing and re-establish freshness before enabling indexed queries | The current checkpoint implementation guarantees atomic replacement visibility, including overwriting an existing checkpoint on Windows. It syncs file contents before replacement but does not sync the parent directory afterwards; this is **not a power-loss durability guarantee**. Stale or missing checkpoints must be reconciled or rebuilt. Existing CLI behavior, RPC protocol, index format, and single-root serving must remain available while shared mode is introduced explicitly. Do not implement sharing by pointing today's servers at the same `--index-path`: their publication path still writes a complete index. ## Implemented generation API `tgrep_core::generations::Repository::discover(root)` invokes Git without a shell and identifies the canonical native common directory. `.git` files, linked worktrees, subdirectories and bare repositories are supported. Independent clones do not share identity, even with equal tree OIDs and remotes. `Repository::git_dir()` retains the discovered worktree's metadata directory so symbolic revisions such as `HEAD` resolve there, not at another worktree's HEAD. Ambient `GIT_*` environment overrides and replace refs are ignored; Git failures, missing objects and unsupported native/index paths are explicit errors. `GenerationManager::new(repository)` uses `/tgrep-bases-v1//`. `with_storage(repository, storage)` accepts an existing trusted external directory and uses a repository-identity subdirectory. It rejects storage in registered worktrees, Git metadata or another index snapshot. Stores and their ancestors must not be externally renamed or modified while in use. Checks cover the effective canonical repository-identity directory as well as the supplied parent, including an existing worktree or snapshot at that child. | API | Contract | | --- | --- | | `Repository::resolve_commit_tree(revision)` | Resolve exact commit/tree IDs without building or publishing; symbolic revisions use the discovered worktree | | `ensure(revision, profile, predecessor)` | Resolve an exact commit/tree, build or reuse the key; incompatible supplied predecessors error | | `EnsureResult` | Pinned `Arc`, requested commit, and per-request `BuildStats` | | `open(key)` / `list()` | Open exact or list validated published generations; errors never become empty bases | | `Generation::key()` / `commit_oid()` | Serializable exact repository/tree/profile/format/schema key; first publishing commit for diagnostics | | `Generation::base()` | The shared `Arc`; use existing worktree/overlay APIs without changing their validation | | `entries()` / `entry(path)` | Sorted complete tracked membership, Git modes/OIDs/raw sizes and content classification | | `TrackedEntry::matches_worktree_bytes(bytes)` | Decoded-content identity comparison only, not a freshness/visibility/eligibility claim | | `retention()` | `RetentionPolicy::RetainAll`, including after all current pins drop | The versioned profile is `RawGitBlobAutoV1` plus `TrackedRegularFilesV1` and a raw blob-size cap (64 MiB by default; `None` disables it). It invokes the existing automatic decoder, binary classifier and masked trigram extractor on raw blobs, never checkout-filtered bytes. Ignore rules, hidden visibility and binary extensions are deliberately not applied to this tracked-path superset. Destination mode/size eligibility is always recomputed. Only regular-file predecessor entries with compatible indexed content supply reusable postings. Binary entries may reuse their classification, but cannot supply absent postings. Empty and short text entries carry a content identity even with zero postings. Oversized, binary, symlink and gitlink records remain distinguishable in tracked membership; symlinks and gitlinks are not followed or content-indexed. Clean Git status and equal blob IDs do not prove equivalence of worktree bytes. The synchronization layer verifies decoded identities from checkout reads, or overlays transformed/changed files. It also applies worktree-specific membership, including ignored/extension-filtered files, sparse checkout and filesystem case behavior. The profile's optional content eligibility selects whether the extension filter applies ([content eligibility profiles](#content-eligibility-profiles)). Non-UTF-8 tracked names, unsafe relative paths, and paths unrepresentable on the current platform are explicit unsupported errors, not lossy aliases. Native repository paths are retained losslessly. Content/coverage enum versions must advance if their semantics change, independently of the index format. A supplied compatible predecessor provides postings by blob identity, including renames/copies, without reading unchanged blob contents. Changed/new blobs are read and extracted in byte-bounded batches by parallel `git cat-file --batch` readers (bounded by available/namespace workers and the private reservation), with the next batch prefetched while file IDs are assigned and pushed to the existing spill sorter in the serial order, so output stays deterministic; private pressure falls back to one blob at a time. The build then publishes a complete streamed index. Memory includes at most two batches of extracted masks, O(tracked paths) metadata and the bounded sorter arena, not all postings. Without a predecessor the first build extracts the full committed corpus. `BuildStats` counts actual blob reads/bytes, extraction calls, reused indexed files, copied postings, and whether this request published or reused a generation. `predecessor_posting_lists_read` counts decoded predecessor lists; when no indexed paths reuse postings, generation creation does not traverse the predecessor. Empty/short indexed files retain their paths and content identities without triggering that traversal. Strict snapshot opening records per-file posting presence during its existing validation pass, including for older generations; there is no new metadata field, format boundary, or additional posting scan. Publication I/O and metadata enumeration are not eliminated by extraction reuse. One OS file lock per repository store serializes cooperating processes, including different-tree builds; it is released on process exit/crash. A weak in-process cache shares live `Arc` pins and their shared reader/path table. Cold validation/fingerprinting runs outside the process-wide cache mutex, so it does not block unrelated repository cache hits; insertion rechecks for a winner. Staging uses unique `.stage-*` directories on the same filesystem. The complete index is strictly opened, its fingerprint and checksummed tracked metadata validated, and files synced before atomic directory rename to the key's name. Final directories are never overwritten, even if corrupt. Abandoned staging directories are ignored, not advertised or automatically garbage-collected. Atomic visibility is not a parent-directory power-loss durability guarantee. These checks detect inconsistent/corrupt data; storage must still be trusted, not treated as an authenticated format for adversarially rewritten files. The synchronization layer retains the generation pin with each view and serializes its key inside the delta checkpoint. The generation's `SharedBase` fingerprint still binds the checkpoint to exact index bytes. Reopening that key is not readiness: reconcile the worktree before enabling indexed queries. ## Implemented worktree synchronization API `tgrep_core::worktrees::WorktreeView::new(root, pin, options)` validates that the canonical root is an actual worktree root in the pinned repository. Independent clones, bare repositories and subdirectory roots are rejected. The exact `Arc`, canonical root and worktree Git directory stay with the view; no mutable base/flush handle is exposed and no base migration occurs. | API | Contract | | --- | --- | | `WorktreeOptions` | Existing `MetaWalkOptions`, bounded `hint_capacity`, optional existing private `checkpoint_directory` | | `invalidate_path(relative)` | Close query gate; queue a file/subtree hint, including old/new rename paths; invalid input errors and forces full repair | | `invalidate_all()` | Close gate and require full verification for startup, overflow, Git/config changes, missed events or polling uncertainty | | `refresh()` | Rewalk membership/visibility; verify hints/new/stat-changed files or, without hints, all admitted contents | | `reconcile_full()` | Always read/verify content, irrespective of size/mtime/Git-status equality | | `status()` | Ready flag, invalidation epoch, published epoch, pending-path count and full-required flag | | `with_snapshot(closure)` | Guarded query-only view with root/epoch/visibility, resolved paths and read-only candidate opens; verifies pinned-root identity before/after the callback | | `save_checkpoint()` | Ready-only, delta-only atomic `overlay.json`, including exact generation key/root/base binding | | `restore(root, pin, options)` | Explicit errors for missing/invalid/mismatched checkpoints; successful restore remains not ready | | `ReconcileStats` | Actual content reads/bytes/decodes/extractions, base/overlay reuse, copied base files/postings, reads avoided and `hint_lookups` ordered-set probes | The agent runtime subscribes **after construction and before initial refresh**, then forwards native/polling/no-watch inputs through these same invalidations. Each refresh makes one bounded attempt, serialized per view. Files are prepared outside the readiness lock; invalidations can continue to arrive. Overlay, visibility and filename membership publish together only if the captured epoch still matches. Otherwise `ChangedDuringReconcile` leaves the view not ready with full work pending. Retrying replays by full verification; sustained churn must scan or wait, never spin indefinitely. Incomplete metadata walks (including ignore-rule errors), unreadable tracked exemptions, failed reads and detected read instability also keep the gate closed. No base-only success is possible. Native directory/file names are validated before converting metadata-walk paths or recording visibility. Non-Unicode names and literal Unix backslashes produce discovery errors instead of aliasing valid or ignored paths; ordinary native-path full scans remain the fallback. Reconciliation holds a stable root directory handle. The extracted `tgrep_core::rooted::RootedDir` helper (`open`, relative `open_file`, `verify_root`) is also retained once per ordinary server and reused across indexing, watcher and verification passes, rather than reconstructed for each file open. Unix descends with component-relative `openat`, `O_NOFOLLOW` and `O_NONBLOCK`, then verifies the final handle is regular. Windows holds ancestor handles without delete sharing, rejects reparse points and validates the final handle's containment and actual parent before reading. File version and identity are rechecked through rooted handles; a root identity change also prevents publication. Errors leave readiness closed, including directory/link swaps and regular-file/FIFO swaps. Windows keeps the root guard until view drop; agent runtimes must release registrations before removing or renaming worktrees. Ordinary serving retains its root guard for the server lifetime too: stop the ordinary server before removing or renaming its served root on Windows. Each reconciliation advances the epoch, including no-hint full repairs, so successful publication acknowledges earlier invalidation tokens with an equal or later epoch. `WorktreeSnapshot::candidates(plan, prefix, include_hidden)` resolves both base and live IDs before releasing the overlay guard. `files(prefix, include_hidden)` comes from complete walker filename membership, not posting lists. `open_file(relative)` returns a read-only regular-file handle from the view's retained reader, never from a new registration of the root pathname. `with_snapshot` checks pinned-root identity before and after every callback, including empty/file-only queries; failure invalidates readiness and queues full reconciliation. Candidate-open failures are latched and returned as an outer I/O error, with readiness invalidated before releasing the guard even if the callback swallowed the error or subsequently opened another file successfully. A callback may return the owned handle for bounded matching outside the guard; reenter `with_snapshot` and verify the original epoch before publishing buffered results. File contents are not frozen by these handles. Later handle-read errors remain caller-owned: report them and call `invalidate_all()` after leaving the guard, without publishing partial results. Hidden files are included in canonical coverage and filtered at query time, including Windows attributes and explicit hidden-directory scopes. Existing walker rules determine ignore/extension, materialized symlink/submodule and regular-file behavior. Missing, sparse and ineligible base paths are tombstoned; staged, unstaged, committed-divergent and eligible untracked files use whole-file overlays. Restoring the pinned content removes the override; committing divergent content does not. Full passes walk without a metadata-only size cutoff, then bound actual reads to the configured limit plus one byte and classify the bytes read. Eligible content is auto-decoded and hashed before reuse, so CRLF, smudge, encoding, assume-unchanged and skip-worktree cases cannot rely on clean status or index stat data. Verified content-identical copies/renames can populate new overlay paths by copying base masks in one streaming posting pass. Matching existing overlays keep their masks. This saves trigram extraction, **not all repository-sized startup work**. Every refresh walks metadata and membership. Full passes read/verify all admitted content; hinted passes can retain previous evidence for unaffected paths under the explicit event-driven freshness contract. Case-alias hints conservatively reverify; non-ASCII hints and capacity overflow require full verification. Accepted hints are rebuilt from normal path components, so trailing/repeated separators and interior `.` spellings cannot lose subtree invalidations. Absolute, parent-component and leading `.` component hints still error and require full repair. Hint matching probes a `BTreeSet` for the normalized full path and ancestor prefixes, rather than scanning every hint for every file. `hint_lookups` counts these probes (at most path depth per file, each logarithmic in queue size); the regression uses 2,001 pending hints but only five probes for two files. Hinted passes still open eligible regular-file handles for safe metadata verification even when content reads are avoided. Same-size/restored-mtime edits without notifications are repaired by forced full checks, not promised by hints. Successful refresh acknowledges processed inputs, not an atomic filesystem snapshot. Final matching uses `WorktreeSnapshot::open_file` or a private versioned cache, not an ordinary read of `snapshot.root().join(path)` that could follow a raced link outside the worktree. Contents can still change through later writes to the opened file. Raw LF blobs and clean CRLF/smudge checkouts may differ for **every file**. Normalizing line endings would invalidate positional/next-byte masks. Fixtures assert three identical tracked files are read/decoded with zero extractions, versus four clean transformed tracked files requiring four private extractions. The linked worktree's plain `.git` pointer file adds one private extraction in either case; later unchanged passes reuse all private overlays. No transformed-content cross-view cache is implemented. Preparation retains O(paths) metadata plus changed-file masks until publication, in addition to the existing private overlay. A large transformed checkout can therefore require substantial private memory. Checkpoints use the foundation's single directory-bound atomic publication; there are no unprotected auxiliary manifest writes. A configured private checkpoint path must be an existing directory; construction, restoration and saving reject regular-file paths instead of accepting an unusable view. The checkpoint directory is excluded in its entirety (including staging files), along with `.tgrep`, Git metadata directories, generation storage and caller-supplied `walk.exclude_paths`. It cannot be an index snapshot, Git metadata or a worktree ancestor. Storage must remain trusted and not externally renamed while in use. The plain `.git` pointer in a linked worktree follows ordinary walker membership: its filename and content are available with hidden inclusion, subject to ignore and size rules. The named Git metadata directories remain excluded; the file's contents are not followed as a filesystem link. Restoration validates the exact generation key, base bytes and canonical root but does not restore readiness or trusted read evidence; first reconciliation may re-extract restored private postings. Missing/stale/invalid state is an explicit recoverable error, not an empty overlay. Legacy bases remain immutable and retain-all. This fixed-generation `WorktreeView` layer does **not** own native watchers, automatic shared CLI serving, daemon routing/wire schemas or content caches. Existing CLI/server behavior is unchanged unless a worktree is explicitly attached to the daemon described below. ## Daemon wire contract v1 **Deprecated as of tgrep 1.2.1, still supported for existing clients.** New integrations should use the [managed v2 client contract](#managed-client-contract) with an explicit [policy](#managed-policy). The no-policy invocation below continues to select v1; its one-time startup warning goes only to stderr, never stdout or the RPC stream. Run `tgrep serve --shared ROOT --shared-storage EXISTING_EXTERNAL_DIRECTORY`. The runtime supervises this foreground process. `shared attach ROOT --revision REV --lease TOKEN`, `shared refresh ROOT --lease TOKEN [--changed REL ... | --full]`, and `shared detach ROOT --lease TOKEN` are CLI wrappers emitting one JSON result. The new literal subcommand name must be escaped as a pattern: `tgrep -- shared .`. The common Git directory holds `tgrep-daemon-v1.json` (protocol, random-like unique instance ID, repository identity, PID, loopback port, canonical storage path). An OS lock in the same directory excludes a second daemon even with a different storage choice. The listener/worker queues exist before registration publication. Every attached worktree has a separate `tgrep-view-v1.json` in its **actual worktree git_dir**, binding the daemon instance, canonical root, view ID and exact generation. No `serve.json` schema is overloaded. Attach publishes this marker; last detach removes it only if instance/view still match. Discovery stops at the nearest Git boundary and validates capabilities, root/base/profile and readiness before emitting any rows. Merely having a daemon is not attachment. Client discovery carries one validated root/repository through daemon discovery and lookup (three Git subprocesses total per attached CLI query). Daemon query workers launch no Git subprocesses: they reuse the attached repository, rechecking canonical root, nested boundaries, gitfile target and common-directory identity. Real Git trace2 process-start events assert these counts in integration tests. RPC is one newline-delimited JSON request/response per TCP connection, loopback only. All requests have this envelope (including `hello`): ```json {"jsonrpc":"2.0","protocol":1,"instance":"FROM_DAEMON_MARKER","repository":"FROM_DAEMON_MARKER","id":1,"method":"hello","params":{}} ``` Successful results repeat `protocol`, `instance` and `repository`. Errors have `error.code` and `error.message`, never success-shaped empty results. V1 rejects unknown envelope/parameter fields, malformed options and incompatible identities. Clients require the matching request ID on successful responses. A valid error object (integer code, string message, no result) may instead have an explicit null ID when rejected before request parsing, preserving connection-queue saturation diagnostics. Missing/mismatched IDs and malformed errors are rejected. Search RPCs must omit `passthru` or set it to `false`; `true` is rejected with a scan-required error because indexed candidates omit nonmatching files. The CLI keeps `--passthru` on its existing filesystem-scan path when it emits all lines. Canonical worktree roots must be UTF-8 for JSON transport. CLI and server reject unsupported roots, including Unicode aliases to them, before serialization or generation publication; the core API's native-path support is unchanged. Gitfile and `commondir` targets retain native path bytes on Unix, even when metadata lives outside a UTF-8 worktree. Discovery and identity revalidation share the same decoder without spawning Git on the daemon's hot query path. The existing `ignore` dependency is vendored at its locked version with the same native-path correction in Git ignore-source discovery; ignore precedence, nested repository boundaries and malformed-ignore failures are preserved. See [dependency provenance and distribution](vendor/ignore/PATCHES.md). Instance IDs prevent stale metadata/port reuse; they are not authentication against hostile local processes. Storage, repository metadata and the loopback user environment are trusted. | Method | `params` | Result-specific fields | | --- | --- | --- | | `hello` | `{}` | `capabilities`, `profile`, `retention`, `limits` | | `attach` | `root`, `revision`, `profile`, `lease` | `view`, `lease`, `root`, `generation`, `requested_commit`, `ready`, `attach_build` | | `lookup` | `root` | Current view descriptor and status; does not create a lease | | `status` | `root`, `view`, `query: {}` | Readiness, pending/full flags, epoch, last error/success, watcher mode, sharing/extraction counters | | `refresh` | `root`, `view`, `lease`, `changed: []`, `full: false` | Descriptor plus `processed_epoch`; no hints means full verification | | `detach` | `root`, `view`, `lease` | `remaining_leases`, `detached`, `view`, nullable `registration_warning` | | `files` | `root`, `view`, `query: {scope, hidden, max_depth}` | Root-relative `files`, `epoch`, `ready`, `generation`, `backend: "shared-v1"` | | `search` | `root`, `view`, `query` | Root-relative match/context rows, `file_stats`, `index_stats`, `epoch`, same view/base/backend fields | The `recoverable-attach` capability requires a caller-owned `lease` on every attach RPC: 1-128 ASCII letters, digits, hyphens or underscores. Persist a unique token before sending. Repeating a live token with the same canonical root, literal revision and profile replays the original attachment and `attach_build` statistics, without allocating another lease or re-resolving symbolic revisions. It works at the lease limit and after response loss. A token already used by another root/revision is an error. Distinct callers use distinct tokens. `lookup` followed by `detach` also releases a known token without its original response; the CLI does that lookup automatically. Tokens are scoped to the daemon instance, are not authentication credentials, and are forgotten on detach or restart. After restart, explicitly attach again with fresh tokens. Final detach always releases the view/lease even if registration cleanup fails; it reports the failure in `registration_warning` and daemon stderr. Missing markers are harmless. Corrupt or foreign markers are not removed without matching instance/view identity, so their continued presence selects safe fallback. CLI `--lease` is optional for interactive convenience: if omitted, a generated token is printed on stderr before transmission and must be captured and supplied on retry. Runtime integrations should always supply their own token. The `profile` is required on attach: ```json {"content":"raw-git-blob-auto-v1","coverage":"tracked-regular-files-v1","max_blob_bytes":67108864} ``` #### Content eligibility profiles The profile may also carry `"eligibility":"content-only"`. The default, `extension-and-content`, is omitted from the wire form, so default profiles, generation keys, storage names and `hello.profile` are unchanged. Daemons that support the optional field advertise the `content-eligibility-profile` capability; older daemons reject the unknown field on attach. | Stage | `extension-and-content` (default) | `content-only` | |-------|-----------------------------------|----------------| | Committed base | Raw tracked blobs, content-classified; no extension rule | Same corpus, but a distinct generation key and storage name | | Overlay/verification | Binary-extension paths have no content and hide the base entry | All listed files are decoded and classified by content; clean text reuses the base, NUL content is excluded | | Incremental and full refresh | Same path rules | Same path rules; the profile is never re-chosen | | `--files` | Unaffected | Identical filename membership | Ignore, hidden, size, symlink and NUL/binary classification are the same in both profiles. The profile is part of the pin: a join, accept-current attach or migration with a different profile fails, so every client of one worktree root must request the same eligibility. Checkpoints restore only into the same profile. Unmanaged v1 leases accept only the default. To change a root's profile, release every lease; a fresh `views.attach` with an explicit `revision` and no `accept_current` then replaces the inactive view (retryable `previous-root-readers-active` while old readers drain). No close RPC is needed. Queries state their eligibility. The CLI sends `content_eligibility: "content-only"` in the search query when run with `--content-eligibility content`, and omits the field otherwise. Before using a discovered view, the client compares the requested eligibility with the view's generation profile. The server makes the same check for content queries (not `files`). A mismatch in either direction is refused with the shared-fallback warning, and the client scans the filesystem using the requested rule. A legacy view is never served under a wider-coverage request, and a content-only view is never served to a default request. Older clients see a non-default profile and fall back. Content-eligibility queries never use an ordinary server or local index. A runtime that wants ripgrep-compatible extension handling should check `content-eligibility-profile`, attach with: ```json {"content":"raw-git-blob-auto-v1","coverage":"tracked-regular-files-v1","max_blob_bytes":67108864,"eligibility":"content-only"} ``` and query with `tgrep --content-eligibility content ...`. With `--stats`, a managed answer reports `(via shared daemon v2)`. A content-only base is built and stored separately from a default base for the same tree. `query` uses the existing search RPC fields built by `server_search_request`: pattern/extra_patterns, matcher flags, types/type_add/type_clear, globs, context/output-detail fields, `count`/`count_matches`, hidden, scope and max_depth. `count: true` returns one `{"type":"count","file":"...","lines":N}` row per matching text file. `count_matches: true` returns one path row plus the original per-file totals. Binary markers retain their counts and client-side visibility filtering; `stats` still returns the original match totals without restoring content rows. Legacy non-counting requests keep their existing response shape. JSON clients additionally send `search_stats: true`. The response's `search_stats` array contains `{"file":"...","bytes_searched":N,"binary_offset":null}` for every candidate searched, including those with no matching rows; a binary candidate carries its numeric detection offset. These receipts are collected at matching time, charged to the existing response budget, and filtered by the client's normal scope/binary visibility rules. They require no extra reads and do not claim full-scan work. Missing or malformed receipts are rejected before output; older shared servers reject the new query field, causing the usual diagnosed scan fallback. Queries without this field keep their prior shape. Only default automatic decoding and the 64 MiB profile are compatible. `scope` is empty or an existing relative directory, never a regular file; it cannot escape the root or cross a nested repository. Content reads use no-follow root-contained handles, never another worktree's content cache. Candidate collection captures an epoch; a concurrent invalidation rejects the response before output. Read failures invalidate the view and return errors. Views are shared only for the same root and exact tree/profile. Independent lease tokens release independently, have no automatic expiry, and are invalid after restart. Reattachment resolves and compares the requested tree with the existing pin before any generation build/publication; a different commit with the same tree is compatible when using a new lease. New generations may reuse a compatible currently pinned predecessor; existing views never migrate. All generations/checkpoints are retained. Generation and overlay directories are separate under external storage. A saved delta is keyed by canonical root and exact generation and is full-revalidated on reattach. Unavailable/corrupt generations or checkpoints cannot reveal the base. Normal CLI search/files/status automatically use an existing attachment. `--shared` additionally forces shared-only discovery. Explicit `--index-path` preserves ordinary index intent unless combined with `--shared`. Stale/missing forced registrations, incompatible options, saturation and not-ready states produce meaningful scan diagnostics, bypassing all ordinary index shortcuts. Marker entry presence includes dangling symlinks, so corrupt metadata cannot silently select a stale ordinary index. `status` reports an error rather than misleading legacy status. `--no-index` always scans. Positive indexed globs filter the admitted corpus; scans can reinclude ignored files, as with ordinary indexes. Limits are aggregate and visible in `hello`: 32 views, 256 leases, 8192 native subscriptions and 16384 hint slots by default. `--shared-max-views`, `--shared-max-leases`, `--watch-budget`, `--watcher-queue-cap` select them. Watch/hint budgets are fixed per-view shares of the configured maximum; there is no borrowing. Bounded nonrecursive registration reuses the ordinary watcher registry on each platform; too many directories or registration errors select polling rather than partially claiming native coverage. There is one serialized index/reconcile worker and two separate query workers, so repairing A does not take a repository-global search lock or prevent ready B from searching. Work queue capacity is max_views; query queue is 16, incoming connection queue 32. Two routers enforce 1 MiB requests and timeouts. Responses over 64 MiB fail explicitly without truncation. An encoded-byte budget checks borrowed rows before allocating their JSON objects, counts filename/statistic/escaped-string overhead across files, and bounds final serialization. Existing per-file matching state and candidate/path tables remain proportional to the admitted input corpus; the response cap is not a total-process memory cap. There is no content cache. Mapped generations, path tables and private postings still scale with repository/overlay size, not with a fixed memory cap. Native directory registration and event filtering exclude the root's `.tgrep` storage directory, not eligible hidden directories such as `src/.tgrep`. Callbacks immediately invalidate the appropriate view; hints are bounded and overflow/unknown events request full repair. Registration precedes initial reconciliation. A failed or concurrently invalidated pass stays not ready and retries; no base-only window exists. Consecutive failures use completion-based exponential retry delays of 1, 2, 4, 8, 16 then 30 seconds; success or explicit refresh resets the delay. Explicit refresh does not wait for the retry deadline. Status exposes `reconcile_attempts`, `consecutive_failures`, `retry_delay_ms` and the retained `last_error`. Daemon readiness stays closed until both the core refresh and checkpoint publication finish, including failed checkpoint retries. Candidate files are opened through the view's retained snapshot reader. Every query ends with a root-verified snapshot, including empty and filename-only responses (strict views also reject a changed epoch there); replacing the directory cannot pair old candidates with a new root. Local-index and shared searches amortize ancestor opens across bounded groups of up to 64 consecutive siblings. Each group retains one parent chain, not an unbounded directory cache or a batch of file contents. Shared candidate batches retain the generation and root protection, validate each candidate through its snapshot, and verify ancestor identities before returning results. Open/read or final ancestor-verification failures close the view for repair (eventual managed v2 views instead skip that candidate and queue it as a path hint; see **Eventual serving**); deadline and snapshot-validation failures do not masquerade as filesystem failures. This does not change admission, matching, response limits or fallback semantics. In the legacy v1 daemon, both auto and poll modes perform full verification every `--poll-interval` seconds after completion (default 120), also repairing missed bytes, ignores and external Git/global configuration changes. Native notifications allow earlier incremental repair. `--no-watch` disables both periodic and native refresh, not initial reconciliation or explicit runtime refresh. Neither ready nor a processed epoch promises the newest possible bytes. Integration tests measure three unchanged tracked files as 3 reads/decodes, 0 private extractions, and the same `Arc` in both views. A linked worktree's ordinary `.git` pointer file adds 1 read/decode and 1 private extraction on initial reconciliation (4 reads and 1 extraction total); it remains visible with `--hidden`, while real Git metadata directories stay excluded. Four CRLF-transformed paths require 4 private extractions plus the linked gitfile's 1 (5 total), with 0 new extractions on no-op verification. Sharing never promises to eliminate startup reads or checkout transformations. ### Stateful parity and runtime lifecycle exercisers The existing `shared_daemon` integration fixture also runs two bounded seeded sequences (`stateful::`) and a process supervisor (`runtime::`). These are test-only helpers, not an integration SDK or an automatic lease collector: ```sh cargo test -p tgrep-cli --test shared_daemon -- stateful:: runtime:: cargo test -p tgrep-cli --test ripgrep_compat indexed_json_preserves_utf8_match_and_context_offsets ``` The parity sequences compose edits, additions, deletes, renames, staged and unstaged versions, committed divergence, ignore changes, restoration to the pin, reset/checkout/rebase, sparse materialization, CRLF checkout bytes and daemon restart across three real worktrees. A small independent file/line model checks isolation and membership, including hidden gitfiles, empty files and filename-only binary entries. CLI line/count/match/file/JSON output is compared with `--no-index`; scans exclude the main worktree's actual `.git` directory because shared indexes intentionally exclude Git internals. Linked gitfiles remain included. Unicode `-F -i` folds are checked through both base and overlay paths, ignores and restarts. JSON comparisons retain offsets, lines, submatches and match totals; only timing and backend work counters are normalized. The accompanying regression checks multibyte UTF-8 before later match/context rows through scans, local indexes and the ordinary server. Every comparison requires the shared backend (RPC identity plus CLI stats/query counters); a scan fallback cannot count as passing parity. Writes stop before acknowledged hinted/full refresh barriers. Native-watch sequences additionally require an event-driven publication before explicit refresh, with bounded retries only for documented not-ready/concurrent-invalidation responses. This tests processed-input contracts, not a promise that readiness proves latest disk bytes. Defaults use seed `1742026`, one round per watch mode and small fixtures in normal CI. Set `TGREP_SHARED_SEED` to a decimal u64 and `TGREP_SHARED_ROUNDS` to `1..16` for replay/stress; invalid values fail rather than silently changing coverage. Failures print the seed, round count and ordered operation log. Roles and edit payloads vary by seed while prerequisite-dependent transitions remain ordered. Each sequence has a 300-second-per-round overall deadline. The supervisor persists caller-owned tokens outside worktrees before spawning actual client processes. It combines simultaneous callers, repeated/lost-response attach at the lease limit, explicit crash/timeout cancellation, journal-based abandoned-lease recovery, daemon restart with fresh tokens, budget release and detach-before-move/remove while a sibling keeps querying. Abandoned v1 leases remain live until the runtime explicitly releases them; they have no expiry or GC. File-backed child output avoids pipe deadlocks, RPC/start/stop waits are bounded, and owned child guards kill/reap only their own PIDs on failure. The ignored `runtime::runtime_client_process` test is a subprocess entry point invoked by the supervisor, not an additional standalone test to run with `--ignored`. Run watcher qualification on native filesystems (including native-ext4 WSL checkouts rather than DrvFS); no platform is silently skipped. ## Implementation status and rollout The implemented increments are additive; shared serving requires explicit opt-in: | Surface | Current status | | --- | --- | | [`SharedBase`](tgrep-core/src/shared.rs) | Opens a validated reader once; creates independent worktree-rooted views sharing its `Arc` | | [`HybridIndex`](tgrep-core/src/hybrid.rs) / [`LiveIndex`](tgrep-core/src/live.rs) | Reuses existing candidate merging, replacements, and tombstones | | Overlay checkpoints | Atomic delta-only save/restore; validate format, root, paths, and exact base fingerprint | | Base identity | Index fingerprint plus repository/tree/profile/format/schema generation key | | Base immutability | Generation manager stages/validates/publishes once; external callers must not mutate mapped files | | [`Regression coverage`](tgrep-core/tests/shared_worktrees.rs) | Sharing/isolation, masks, tombstones, restoration, compatibility, repeated saves, and Windows failure recovery | | [`Generation management`](tgrep-core/src/generations/mod.rs) | Canonical common-dir identity, raw committed-tree builds, incremental posting reuse, cross-process deduplication, pins and retain-all | | [`Generation coverage`](tgrep-core/tests/generations.rs) | Temporary repositories/worktrees, thread/process races, content transformations, immutable old readers, errors and interrupted publication | | [`Worktree synchronization`](tgrep-core/src/worktrees.rs) | Pinned private views, actual-content verification, atomic readiness/membership, bounded invalidations, full repair and bound checkpoints | | [`Synchronization coverage`](tgrep-core/src/worktrees/tests.rs) | Real divergent worktrees, scan/candidate parity, CRLF/smudge/decoding, sparse/assume-unchanged, filtering, epochs, errors and extraction counts | | Legacy v1 CLI/daemon routing and native watchers (deprecated, supported) | Versioned leases, discovery, bounded workers/watchers/hints, full repair, delta checkpoints, strict scan fallback | | [Managed lifecycle](tgrep-core/src/managed/mod.rs) | Isolated guarded storage, transactional catalog, owner lifetimes, versioned migration, allocation admission, bounded collection and offline maintenance | | [Managed CLI/RPC](tgrep-cli/src/serve/shared/managed.rs) | Negotiation, asynchronous operation receipts, owner holder, recovery, adaptive scheduling, live diagnostics and atomic idle shutdown | Shared-base validation rejects mismatched empty lookup/posting sections and metadata counts inconsistent with the opened sections, while allowing empty indexes and short files. Shared snapshots require aligned, contiguous posting ranges covering the entire postings section, valid trigrams, strictly increasing valid file IDs in each posting list, and nonzero location masks. Checkpoint destinations reject trailing separators and current-directory suffixes, and exclude the base and its descendants by directory identity, even if the base has been renamed. Unix staging, replacement, and cleanup are relative to an open parent-directory handle; Windows holds non-delete-sharing handles on the canonical parent and all its ancestors until publication completes. A renamed/replaced pathname cannot redirect publication into the base. Root identities preserve the existing JSON string encoding for Unicode paths and use tagged platform-native units for non-Unicode Unix/Windows paths; older readers reject the latter representation. Both modes keep one shared base plus one private overlay per worktree, using the same builder, matcher, rooted reads, reconciliation and query pipeline. Managed lifecycle does not introduce a second search engine or a shared mutable branch overlay. ## Managed storage and lifetime boundary `serve --shared ROOT --shared-storage CACHE_PARENT --shared-policy POLICY_FILE` opts into protocol/storage schema 2. `CACHE_PARENT` must already exist and be trusted. The policy selects one immutable namespace storage kind: | Kind | Reader/storage contract | | --- | --- | | `managed` | Guarded generation/checkpoint format; published objects can be collected under bounded policy | | `compatibility-retain-all` | Adapter over legacy generation storage; v1 participants remain fixed-pin and legacy objects are never collected | | No policy file | Deprecated but supported v1 daemon, with unchanged layout, flags, leases and retain-all behavior | The managed namespace is `CACHE_PARENT/tgrep-managed-v2/`. Repository identity derives from the canonical native Git common-directory identity, not a remote URL, branch name or disposable worktree. Two repositories may use the same cache parent without sharing catalogs, accounting or authority. ```text namespace.json schema, namespace/repository and filesystem identities owner.lock exclusive namespace owner: daemon OR maintenance activity.lock admission/idle boundary, including independent readers catalog.sqlite transactional object/reference/operation/current-view catalog guards/ per-incarnation and worktree-root lifetime guards owners/ issued owner guard files and sealed instance challenges objects/ managed generation or checkpoint incarnation ``` Managed generation files use `paths.tgm`, `lookup.tgm`, `postings.tgm`, `meta.tgm` and `generation.tgm`, incompatible magic and file-table format `0x4d320001`. Old and unguarded readers reject them even under renamed legacy filenames. No legacy-readable intermediate is published inside the managed namespace. Ordinary index commands also refuse a store directory itself and paths beneath its 64-hex repository namespaces, compared lexically and case-insensitively; an unrelated directory that merely shares the store name stays usable. Existing v1 stores are not converted or retroactively made collectible. Managed namespaces also require a supported content-authentication format. Missing or incompatible producer proof is not upgraded by hashing whatever bytes happen to be present. This boundary is separate from protocol negotiation: ordinary indexes and default v1 retain-all namespaces keep their existing format. Each publication has a fresh physical incarnation independent of its logical repository/tree/profile key. A delayed deletion of an old incarnation cannot delete a replacement for the same logical key. Catalog, namespace and instance IDs are opaque 32-character lowercase hexadecimal identifiers, not paths or authorization credentials. `managed::open_generation` and the managed generation/checkpoint adapters acquire OS-backed protection before opening data. Protection follows escaped `Arc`, `SharedBase`, `IndexReader`, derived worktree views, snapshots and rooted candidate handles, including independent processes. It also covers build predecessors, checkpoint restore and both sides of migration. Local `Arc` counts alone are not reclamation evidence. Per-object guards allow an unrelated obsolete object to be collected while ready views continue serving; the namespace activity guard is used for idle shutdown, not to block all GC. Guard acquisition is authorized against the current catalog state while retirement is excluded. A stale root, owner claim or generation open cannot probe a retired guard and disrupt its physical reclamation. Releasing the last root handle withdraws that incarnation; bounded maintenance reclaims its control file separately, so successful handle release does not wait for physical GC. Owner release and released-view draining use that same withdrawal path. An unrelated control-file cleanup cannot fail a committed logical owner release; the retired root guard remains accounted for until bounded maintenance removes it. Root-lifetime verification and physical-cleanup failures remain explicit errors in their respective operations. Namespace ownership is rooted in external storage and survives repository deletion. Live startup also coordinates through the repository's common-directory ownership lock, excluding simultaneous legacy and managed daemons. Catalog and filesystem identities are checked together. Traversal/deletion is relative to validated directory handles, with native Windows sharing/reparse checks and Unix no-follow identity checks. Unknown files, symlinks, reparse points, hard links, replaced members and foreign directories are preserved with diagnostics. There is no broad directory sweep and no online deletion of another namespace. Managed storage must remain private to tgrep. Clients use the management API; they must not edit catalog records, member files or ownership guards. Native locks coordinate cooperating tgrep readers and writers. Content verification additionally detects external changes, but the storage owner is not a security boundary against another process able to alter its files and authoritative catalog. In particular, a final identity/content-version check followed by a filesystem operation is not an atomic conditional deletion against an arbitrary concurrent foreign writer. Do not modify the namespace while maintenance runs. This limitation never authorizes ignoring detected changes: modified, unknown or incompatible entries encountered before a destructive unit are preserved and reported, even when doing so leaves a storage-budget shortfall. ### Producer-content authentication Immutable members have producer-origin BLAKE3 proof for fixed 4096-byte blocks, including the exact length of a partial final block. Their complete descriptor binds the intended total length, block count and ordered proof manifest to the physical incarnation and member. Proof is derived from byte slices accepted by the writer, not later filesystem observations. An empty member also requires a complete descriptor; an absent descriptor is not evidence of an empty file. Proof rows consume catalog/database/WAL capacity, and bounded hash buffers and proof batches consume charged private working memory. They are not an uncounted sidecar or a second full payload copy. Admission can therefore fail on metadata or private-work limits before the payload storage target is reached. The original creation handle establishes file identity and ownership-sensitive metadata. Verification checks regular-file/single-link structure, permissions, owner/group or security descriptor, and relevant native attributes as well as content. Basic timestamps, including Windows ChangeTime, are neither a seal nor an unchanged-content fast path. Bookkeeping-only metadata drift can be accepted only when producer content and ownership-sensitive metadata still match. On Windows, re-recording a closed producer before generation publication authenticates every original block and the complete manifest, even if basic timestamps are unchanged. The native deny-write/delete handle stays open through the catalog update; identity, ownership and length must still match, and no producer seal is replaced. This accepts metadata-only ChangeTime drift without an EA allowlist or a timestamp-based content shortcut. Verification uses bounded, charged buffers. With a work reservation it uses that reservation's admitted deadline and cancellation/version checks, not a new timeout from mutable policy. Unreserved observations receive a bounded deadline from current policy. Open producers and withdrawn or partially reclaimed members do not qualify. ## Managed policy The policy file is a complete JSON object; fields have no silent unlimited defaults, and unknown fields/unsupported schemas are errors. This illustrative policy opts into managed storage but retains fixed-pin/retain-all behavior: ```json { "schema": 2, "storage": "managed", "retention": {"mode": "retain-all"}, "advancement": {"mode": "fixed"}, "work": { "max_views": 32, "max_leases": 256, "workers": 2, "queue_items": 64, "staging_bytes": 536870912, "private_work_bytes": 134217728, "sort_buffer_bytes": 1048576, "blob_bytes": 8388608, "operation_timeout_ms": 120000, "page_objects": 128, "max_cursors": 16, "cursor_lifetime_ms": 30000, "max_receipts": 4096, "metadata_bytes": 67108864 }, "collection": { "schedule": {"mode": "periodic", "interval_ms": 60000}, "on_pressure": true, "checkpoint_grace_ms": 300000, "generation_grace_ms": 3600000, "max_duration_ms": 200, "max_examined": 128, "max_removed": 16, "max_delete_bytes": 16777216, "chunk_bytes": 65536, "max_pages": 16, "retry_ms": 1000 } } ``` These are example allocations, not machine-independent recommended budgets. Choose `blob_bytes` for the eligible corpus: a too-small input budget fails attachment rather than silently omitting an eligible large file. For bounded retention, replace `retention` with `{"mode":"bounded","target_bytes":1073741824}`. Only managed storage accepts it. For automatic advancement, replace `advancement` with: ```json { "mode": "adaptive", "high_bytes": 67108864, "low_bytes": 16777216, "min_reduction_bytes": 8388608, "min_reduction_percent": 50, "cooldown_ms": 300000, "max_paths": 10000, "max_read_bytes": 134217728, "max_attempts": 3 } ``` `{"mode":"disabled"}` disables periodic collection; `on_pressure` separately controls pressure-triggered collection. Retain-all still permits safe recovery of abandoned staging/control state; it never authorizes reclaiming published generations or checkpoints. | Units/field | Accepted range or invariant | | --- | --- | | Bytes | Positive integers through signed 64-bit maximum unless stated otherwise; not percentages or free-space promises | | Durations | Integer milliseconds, positive and representable by the monotonic clock; grace intervals may be zero | | Views / leases | `1..1024` / `1..65536` | | Workers / queue items | `1..64` / `1..65536` | | Page objects / open cursors | Each `1..1024` | | Receipt count / metadata | `1..1000000` / at least 1 MiB | | Sort buffer | At least 16 KiB and representable by the platform address size | | Private admission | Must cover each worker's four sort buffers, six blob buffers and 256 KiB, plus subsequently admitted work/retained state | | Collection examined / removed / pages | Each `1..1000000`; all limits apply, not just successful removals | | Deletion chunk | At least 4096 bytes, no greater than `max_delete_bytes` | | Adaptive watermarks | `0 <= low_bytes < high_bytes` | | Adaptive reduction / paths / attempts | `1..100` percent / `1..1000000` paths / `1..64` attempts | Policy and allocation versions are separate compare-and-swap domains. The initial local allocation is version 1, has no coordinator or additional storage ceiling, and takes staging/private/work-slot ceilings from the policy. A coordinator can install: ```json { "version": 1, "coordinator": "", "storage_bytes": 2147483648, "staging_bytes": 536870912, "private_work_bytes": 134217728, "work_slots": 2 } ``` Use JSON `null`, not a string, for an absent coordinator or storage ceiling. The update's `expected_version` selects the current allocation; the server publishes the new allocation with that version plus one. Byte values are positive and work slots are `1..64`. Policy and allocation both constrain admission. Updates do not revoke live readers or pretend already-admitted work vanished. Producers capture read limits and both versions at admission; a version change stops them at the next bounded control check (normally within 10 ms) or the forced publication check. Namespace mutations invalidate the in-process control cache. An independent one-second fallback refresh checks persisted versions/cancellation and native owner-guard proof even when that cache has not been invalidated. Deadlines and explicit cancellation are checked on every call; no owner lifetime is inferred from a PID or inactivity. Retry failed work under current versions; do not replay an old allocation version as if it were still current. Reservations cover workers, private capacity, staging, predecessor/successor publication overlap and retained private state. `charged_overlap_logical_bytes` is the current retained-plus-unconsumed reservation charge, not a historical peak or physical disk consumption. Catalog/control storage, immutable data and staging have separate accounting. Allocated filesystem bytes use explicit observed/unavailable results; sparse/compressed allocation is not inferred from logical length. Namespace memory is process-local admitted/retained capacity and mappings, **not an RSS cap**. A coordinator must budget multiple namespaces and its other processes independently. ### Bounded managed reconciliation `work.workers` and allocation `work_slots` bound namespace operations, not the number of files one operation can verify. Within an attachment, discovery and content verification share a namespace-wide concurrency gate, capped at available logical CPUs (at most 64). Discovery also retains the normal walker's 12-thread cap. Verification uses the existing shared Rayon pool, never a pool per worktree. Increasing namespace operation workers is not required to parallelize an isolated attachment. The normal builder's file-count/byte batch planner is reused, **not** its file opening or mmap path. Batches are further split by distinct-parent handle cost, and a namespace-wide handle gate bounds simultaneous parent pins: 256 credits on Unix (descriptor limits) and 8192 on Windows, where each deep parent chain costs many credits and a smaller gate degenerated into near-serial ranges. Credits cover retained and verification chains plus root-check scratch. A single unusually deep Windows parent takes the whole gate alone; its actual memory is still charged. Unix retains only the last ancestor descriptor while keeping the complete identity chain. Unmanaged/v1 reconciliation scopes each batch to one parent rather than accumulating pins for unrelated directories. Siblings share one scoped, handle-rooted ancestor chain per bounded batch, including siblings assigned to different workers. Child containment, reparse rejection, file identity and before/after read checks remain mandatory. Observed ancestor IDs are compared with bounded parallelism when the batch ends and again before assembly publishes; completed batches retain observations, not directory handles. These comparisons detect observed substitutions, not uninterrupted ancestor lifetime: filesystems can reuse IDs after handles close, and paths can change after the comparison. Read containment uses the live root, batch and child handles; root and input-epoch checks independently gate publication. As before batching, reconciliation is not an atomic filesystem snapshot. This is not a new relaxation of initial verified reads or a consequence of disabling native safety rescans. Compute credits, parent pins and input bytes are admitted in that order. Native metadata obtained by a verified open is reused, not reacquired redundantly before reading. Workers drain on cancellation/error before their reservation or checkout handles can be released. Input admission is shared across the namespace: 64 MiB of conservative read charges (six times bounded input bytes plus 64 KiB scratch per active file). As in the normal builder, a single larger input takes the entire input gate and runs alone. Its actual buffers must still fit the operation's private reservation and blob limit. Final directory-verification scratch uses the same byte gate; scoped parent-reader memory is separately charged to the private reservation. Reads also wait for that reservation's active input buffers, not for retained postings/evidence that cannot drain during the pass. File growth invalidates the read instead of waiting for more bytes while holding a partial buffer. Postings, decoded buffers, discovery and retained evidence all remain subject to existing private-memory admission. These estimates are not an RSS cap. If parallel extraction exhausts private headroom, the bounded batch is joined, its temporary outputs are dropped, and it retries once with one worker under the same limits. Serial pressure or any non-memory failure still fails the operation. Changed-file mask tables retain their capacity charge only while they exist; extraction releases temporary growth headroom, and publication releases each input table after consuming it. Live overlay admission accounts for hash-table capacities, including bucket/control storage and per-allocation padding, rather than accumulating a fixed allowance for every posting insertion. Table growth reserves both old and replacement storage before allocation. Retained tables, including each posting set and its allocation bookkeeping, keep a backing-capacity watermark across deletions: tombstones can reduce insertion capacity without freeing buckets. Charges are lowered only for dropped or rebuilt storage, not merely because entries were removed. File replacement removes old entries from the unpublished overlay before admitting table growth; failures leave the published view unchanged. Retained views and checkpoint capture remain charged. This allows full CRLF/smudge overlays to use the existing budget without normalizing bytes, weakening masks or raising limits. Workers verify and decode/hash every required file, compare exact indexed content identities, and extract only differences. A single assembler preserves base, overlay and rename/copy reuse and publishes only after epoch, identity, policy and allocation checks. For an eventual first attachment, the pinned generation is published provisionally after the tracked membership/visibility metadata walk; queries can start while the required full content verification runs. This provisional publication is not `ready`, and its paths have no read-version evidence, so incremental refreshes cannot skip them based on stat metadata. An attach receipt therefore no longer implies readiness; wait for `ready: true` when verified results are required. Legacy v1 `shared attach` is never provisional and still returns a ready view. First verification and restores still read the checkout, including CRLF/filter differences and hidden/untracked membership. A clean Git status is not content evidence. Successful operation receipts expose `result.reconcile`, also available as `last_reconcile` in view status. `hello` advertises `reconciliation-phase-metrics`. `provisional_files` counts paths served from the base before verification. Its `timings` fields are integer microseconds: | Field | Scope | |-------|-------| | `queue_us` | Accepted operation lifetime up to the admission snapshot, including pre-reservation setup | | `base_materialization_us` | On attach, generation open/build and view construction | | `discovery_us` | Root/Git identity, ignore snapshot and membership walk, wall time | | `planning_us` | Prior-state lock, sibling ordering, size/charge maps and batch planning, wall time | | `content_verification_us` | Bounded read/verify/decode/hash/compare/extract and batch aggregation, wall time | | `worker_content_us` | Sum of worker intervals; includes input admission waits | | `worker_parent_us` | Aggregate shared parent initialization/access, including initialization waits | | `worker_open_us` | Aggregate secure child opens and their initial metadata observations | | `worker_read_verify_us` | Aggregate bounded reads, input waits, secure reopens and before/after checks | | `worker_decode_hash_us` | Aggregate decoding, binary checks and indexed-content hashing | | `worker_compare_extract_us` | Aggregate base/overlay comparison, copy lookup and difference extraction | | `parent_verify_us` | Batch-end and final ancestor validation wall time, within content/assembly phases | | `assembly_us` | Copy-posting pass, final ancestor checks and private overlay assembly, wall time | | `checkpoint_us` | Checkpoint writing, verification and publication, wall time | | `publication_prepare_us` | Final view/receipt preparation up to receipt construction; excludes its commit/fsync | | `catalog_control_us` | Admission and operation-scoped periodic/final control checks, including waits | | `namespace_catalog_us` | Aggregate namespace catalog accesses during admission, including other operations/polling | | `worker_wait_us` | Concurrency admission wall time | | `parent_wait_us`, `read_wait_us` | Aggregate parent-handle and input-admission wait time | | `admitted_us` | Elapsed since reservation entry, sampled at receipt preparation | These are **not additive**: worker and catalog measurements overlap wall phases. The worker subphases are diagnostics within `worker_content_us`, not additional wall time. Namespace catalog time cannot be attributed to one attachment when other work overlaps. Final view-commit fsync, receipt delivery, registration and producer teardown remain outside the receipt sample; use end-to-end readiness wall time for acceptance. `namespace_catalog_calls`, `control_checks`, `batches`, `batch_retries`, `parent_batches`, `peak_workers` (admitted concurrency credits), `peak_parent_handles` (conservative parent-handle capacity, not a sampled FD count), `peak_read_bytes` (input capacity plus transient directory verification), and `peak_private_bytes` complement the existing read/extraction/reuse counters. Those file counters describe completed batch evidence; when `batch_retries` is nonzero, timing fields also include discarded parallel attempts. The [RC2 large-checkout measurement](SHARED_INDEX_BENCHMARKS.md#large-checkout-reconciliation-rc2) records a same-binary standalone/attachment comparison, the native phase and resource counters, and the unattributed orchestration/readiness remainders. It qualifies initial warm-base attachment, not elimination of recurring full repair. ## Managed client contract Use `tgrep shared manage ROOT METHOD --params JSON`. It discovers and validates the daemon and emits one `{"ok":true,"result":...}` result or `{"ok":false,"error":...}` failure; failures also have a nonzero exit status. For direct loopback RPC, send a newline-terminated JSON-RPC 2.0 request containing `protocol:2`, the discovered `namespace`, `instance`, `repository`, `method`, `params`, `id` and the private `authorization` credential described below. Successful RPC results repeat those identities and place method data in `result.data`. Never reuse a stale daemon registration solely because its PID or port still exists. Request/response limits are negotiated by `hello` (currently 1 MiB / 64 MiB). Call `hello` before constructing requests. It returns capabilities, storage semantics, indexing profile, effective versioned policy/allocation and directory-sync capability. Copy its `profile` into view requests; a profile mismatch is an error, not a silently different corpus. ### Per-user RPC authentication Loopback alone is not a per-user boundary. Every request to a managed daemon, including `hello`, queries and compatibility-v1 requests, must authenticate before dispatch. Each daemon creates a fresh 128-bit OS-random bearer credential and atomically publishes `tgrep-daemon-v2.auth.json` beside its public `tgrep-daemon-v2.json` registration in the Git common directory. The private file contains `{"registration":,"token":"", "legacy_token":""}`. The v2 request's top-level `authorization` field carries that token. Namespace, instance, owner, lease and operation IDs are not substitutes for it. `shared manage` and normal managed queries load credentials automatically. A direct client must verify native owner-only access, read the bounded private file, and compare its **complete registration**, including endpoint and instance, with discovery before connecting. `tgrep_core::managed::read_private_control_file` performs the native file checks. The daemon advertises `private-rpc-authentication` in `hello`; clients must not fall back to unauthenticated RPC when a credential is missing, stale or inaccessible. Invalid credentials return `permission` / `rpc-authentication-failed`, non-retryable and `not-committed`. A changed public registration is rejected locally as `stale-identity` / `rpc-registration-mismatch`. Private files are created with owner-only access **before writing any secret**: Unix owner UID and mode `0600`, or a protected Windows DACL granting only the daemon's user SID. Readers reject symlinks, external hard links, foreign owners and broader permissions. Darwin extended ACLs are conservatively rejected, including inherited ACLs, rather than assuming mode bits exclude other users. Do not copy these files into logs or shared configuration. Credentials rotate at daemon restart and are removed by identity during normal shutdown. In `compatibility-retain-all`, genuine v1 clients continue to use their existing wire format. Their owner-only v1 daemon/view markers carry a separate private capability in the opaque v1 `instance` field; that value is **not** the public v2 instance ID. The adapter validates it before entering any legacy handler; it cannot authorize v2 requests. V1 responses echo this capability as their `instance`, so keep those responses private too. Default v1 daemons started without `--shared-policy` retain their existing transport and behavior. This boundary excludes privileged OS administrators and arbitrary same-user writers, as does the trusted-storage contract. ### Ownership, operation tokens and attachment 1. Persist a bootstrap token, then call `owners.prepare` with `{"token":""}`. Retry that token to recover the same issued claim after response loss. Persist the result's `claim` object outside the worktree, not the whole owner record or CLI envelope. Claim files are limited to 1 MiB, including whitespace; larger files are rejected before deserialization or acquiring the owner guard. 2. Start and supervise `tgrep shared owner-hold --claim CLAIM_FILE`, keeping its stdin open. After its `holding:true` response, call `owners.register` with `{"claim":}`. A Rust client can instead retain `OwnerGuard`. If typed claim acquisition fails before that handshake, the helper writes exactly one newline-terminated `{"ok":false,"error":...}` stdout line whose managed failure fields (`category`, `reason_code`, `retryable`, `committed_state`, `detail`) are reported as returned, then exits with status 2 and its unchanged `tgrep: : ` stderr diagnostic. Examples are `busy`/`catalog-io` (another catalog writer outlasted the fixed SQLite wait), `busy`/`owner-guard-already-held`, `busy`/`namespace-admission-*` and non-retryable `stale-identity` claims whose namespace, instance or lifetime ended. Claim acquisition writes nothing to the catalog. Claim-file read, size and parse errors are not classified and write no stdout line; neither does any failure after `holding:true`, such as a lost handshake. Reuse the same claim file only after a frame with `retryable:true` and `committed_state:"not-committed"`, after confirming that the failed helper exited, within the client's existing attempt and time bounds. Treat no frame, an unparsable line or any other classification as a failed owner handshake. Clients that accept only `holding:true` fail closed on this frame. 3. Persist a unique lease token and the complete operation token/request before calling `views.attach`. Poll `operations.inspect`, then verify `views.status` or `lookup` reports ready. A routing-ready daemon is not a search-ready view. 4. On teardown, call `views.detach` for every lease and inspect its drain result, then `owners.release` with the claim. Release the holder only when its owned lifetime is over. Do not remove a worktree while retained readers/root handles still protect it. Caller tokens contain 1..128 ASCII letters, digits, hyphens or underscores. Operation tokens have a monotonically allocated, persisted sequence within their owner scope: ```json { "token": {"scope": "", "sequence": 1, "token": "attach-session-a"}, "request": { "root": "", "revision": "", "profile": "", "lease": "session-a", "owner": "", "accept_current": null, "migratable": true, "allocation_version": 1 } } ``` The profile placeholder above stands for the JSON object, not a string. `migratable:false` is a fixed-pin participant. Joining an already active view requires `accept_current:{"view":"","version":N}` after reading its authoritative state. Legacy participants cannot acknowledge an advanced pin. An inactive view requires a fresh attachment with an explicit revision rather than `accept_current`. Reuse the **identical token, sequence, method and request** after response loss. `operations.lookup` accepts the operation token directly, even when the original response containing an operation ID was lost. Reusing a token for a different request is invalid. Persist terminal results before advancing `operations.acknowledge` with `{"scope":"","through":N}`. The durable receipt floor prevents an acknowledged old token from being interpreted as new work. Receipt expiration is an explicit error, never proof that nothing committed. A failed or cancelled receipt is terminal: replaying its token does not execute another attempt. A retryable failure with authoritative `not-committed` state permits a bounded new attempt with a fresh operation token, after checking that the expected view/version and exact pin still hold. Keep the original lease token and attachment inputs unchanged; do not silently accept a newer version. An uncommitted operation can already have saved lease intent and progress; the fresh operation token resumes that same lease rather than duplicating it. For example, native catalog writer contention can return `busy`/`catalog-io` without committing a view. Committed or unknown outcomes require recovery, not retry-as-new. Bound both attempts and total waiting time. For an initial attachment there may be no committed pin yet. Use the catalog `lookup` and `views.recover` results to distinguish an absent root from a saved pending exact intent; neither a failed receipt nor an absent root proves that all preparation state was rolled back. Retain the owner and lease identity, check any saved pending view/version/target, and retry only an already captured exact commit, not a freshly resolved symbolic name. An acceptance or completion whose response was lost still requires the original operation token, even if the view is now published. Reserve a fresh sequence for each authorized new attempt and start subsequent operations after the last sequence consumed. Ownership is bound to an issued namespace/instance challenge and the identity of an OS-locked guard. Death is positively proved only by acquiring that same guard. Missing/replaced/inaccessible files, PID reuse, disconnected clients and elapsed time are not death evidence. Unknown proof protects the owner. A registered lifetime cannot be reacquired after it ends. Prepared-but-unregistered claims are not automatically treated as dead. Legacy ownerless leases retain their original no-expiry semantics. On restart, negotiate the new instance and establish fresh owners/leases. `views.recover` returns the durable exact current pin/version/checkpoint separately from original attachment intent. Recover lost operation results as well; do not mistake the original starting revision for the current base or infer readiness from a saved checkpoint. Restored contents are reconciled before indexed use. Unavailable Git objects, absent/evicted cache and incompatible/corrupt metadata are distinguishable outcomes. ### Advancing and reconciling views `views.advance` uses the operation envelope above with this request: ```json { "view": "", "root": "", "expected_version": 1, "target_commit": "", "profile": "", "owner": "", "allocation_version": 1 } ``` It deduplicates/reuses a compatible target generation, privately prepares a complete replacement overlay/membership/checkpoint and forwards invalidations to preparation. The old valid view and unrelated worktrees keep serving. Publication requires a reconciled input epoch and expected version; current commit/generation/version/checkpoint and the receipt commit transactionally before acknowledgement. The complete in-memory state then swaps atomically. Already-issued snapshots keep the old generation and root protection. Cancellation before commitment preserves the old authoritative view. Cancellation, timeout or response loss after commitment cannot undo the new pin: inspect the operation and authoritative record. Failed/concurrently invalidated preparation has bounded retry and releases or conservatively charges its staging; it never publishes a base-only or partially reconciled ready view. If commitment succeeds but the in-memory swap fails, readiness stays closed until reconciliation restores the authoritative publication. Recovery compares the full pin/checkpoint/epoch binding, not just the view version: an ordinary refresh can commit a new checkpoint without advancing the version. The live scheduler and explicit refresh can repair this state without restarting. Version-checked invalidations use the durable current version throughout repair; already-issued readers retain their original protections. `views.refresh` takes `view`, `expected_version`, `owner`, `allocation_version` in an operation envelope and performs full verification. `views.invalidate` directly accepts `view`, `expected_version`, `owner`, `changed` (relative paths) and `full`; include both rename paths. A processed epoch acknowledges observed inputs, not every concurrent filesystem write. Native/poll watching and full repair use the same reconciliation path. Git metadata events use the most specific matching metadata root, including a linked worktree's private Git directory nested under the common directory; lock-file traffic is ignored. **Eventual serving.** Managed v2 daemon views favor availability over freshness. After a view's first publication, `views.status` and `status` report `serving: true`, and queries are answered from the latest published snapshot while later invalidations wait for reconciliation. A newer input epoch no longer makes a query return `busy` / `view-input-pending` or `checkpoint-not-current`, and does not cancel an in-flight query with `query-input-changed`. `ready` keeps its stricter meaning: the publication covers the current input epoch and no invalidation is pending. It still drives scheduling. A reconciliation pass takes the queued path hints and publishes what it reconciled even when newer events arrived meanwhile; those newer hints stay queued for the next pass. A partially reconciled pass is never published. A failed pass keeps the previous publication serving and queues a full repair. Results can therefore lag disk by the reconciliation backlog. Candidate bytes are read live from disk, so a candidate that vanished or stopped being a regular file since the publication is skipped and queued as a path hint instead of failing the query or forcing a full pass, and a publication made during the query does not fail it. Query responses carry `lagging: true` while invalidations newer than the served publication are pending. Use `--no-index` when current disk bytes are required. Legacy v1 views and the core `WorktreeOptions` default keep strict, fail-closed reads. Readiness is a snapshot, not a notification-drain barrier: a later event can advance the input epoch. Epoch advancement alone also need not acknowledge a particular write. To qualify a change, wait until `views.status` reports `ready: true` with an input epoch newer than the write, then verify the expected content through a managed response (`backend: shared-v2`), including empty results. Bound retries of known retryable invalidations; never count a CLI fallback scan as indexed success. `--no-index` remains the way to search current disk bytes without relying on watcher delivery. Queued background reconciliation can be satisfied by a later completed refresh. Under the view's work/publication gates, tgrep checks the exact current pin, checkpoint and input epoch before completing such a job without changing the ready view or invalidating its readers. Its original token/request is retained; the terminal receipt reports `coalesced: true`, the existing reconciled epoch and zero new reconciliation work. This does not postpone polling verification. Explicit refresh always verifies; polling repair, watcher uncertainty and rescan events invalidate for full verification before queuing reconciliation. A newer invalidation therefore cannot be skipped by the coalescing check. **Managed update policy:** healthy native-watcher views receive a serving periodic full verification. Once `--poll-interval` seconds (default 120) have elapsed since the view's last completed full pass, the scheduler queues one `reconcile` operation with `verify: true` without invalidating the view. It uses the ordinary operation worker, deadline (`operation_timeout_ms`) and CPU/input/handle budgets. It reads, decodes and hashes every admitted file and compares searchable membership and visibility against the published snapshot. Queries keep using that snapshot throughout. If nothing differs, the operation completes with `{"verified":true,"changed":false}` and publishes nothing. The first detected difference marks the view stale, which closes `ready` (queries keep using the previous publication); the pass then publishes a full repair and reports `changed: true`. Silently missed changes are therefore repaired within one interval plus verification time, not left stale indefinitely. Deadline exhaustion and cancellation of a serving verification leave the view ready but unverified; the outcome is `failed` or `cancelled`, and the next attempt waits a further interval. Other verification errors close readiness. Watch errors/overflow, explicit full invalidation, observed Git/ignore changes and normal invalidating events keep their existing fail-closed semantics and take precedence over a pending verification. Continuous mutation can postpone verification, because those events already require reconciliation. Older binaries reject the `verify` journal field and fail closed. There is still no integrated lossless checkout change journal; timestamps, Windows `ChangeTime`, clean Git status and a drained watcher queue do not prove absence of unreported writes (including active/mapped writers). An unavailable/inactive journal is not an empty journal and does not justify skipping initial content verification. Managed native registration uses recursive roots on Windows (`ReadDirectoryChangesW`) and macOS (FSEvents), without enumerating worktree descendants first. It watches the canonical worktree and Git/common metadata roots, omitting subscriptions already covered by another recursive root. New and moved-in directory trees are covered by those same subscriptions; Git metadata events still use the most specific metadata root. Cache and irrelevant Git events are filtered after delivery, not by pruning OS coverage. The per-view share of `--watch-budget` is unchanged: the startup budget divided by the policy's `work.max_views` (8192 / 32 = 256 for the runtime defaults). It counts registered roots on these backends, not descendant directories. Typically a main worktree costs one subscription and a linked worktree costs two; external Git layouts may need three. Each view owns and releases its own subscriptions, including any shared metadata-root subscription. Linux/Android retain bounded, non-recursive per-directory registration; other backends are also conservatively per-directory. Their directory-count, entry-traversal and 100-ms planning bounds are unchanged, and large trees may still require polling. This process-local budget does not reserve or raise OS capacity; another process can exhaust it first. Registration failures retire the affected view's entire watcher and select polling, rather than advertising partial native coverage. Ordinary-server and legacy v1 registration are unchanged. First attachment/restore, explicit full refresh, reported watcher error/overflow or rescan, and observed Git/ignore changes retain full verification and the existing publication gates. Normal native file events still invalidate their paths. Explicit `--watch-mode poll` and actual native-watcher unavailability, failure or budget exhaustion use periodic full reconciliation as their update mechanism; they close `ready` for each pass (the previous publication keeps serving) and wait `--poll-interval` seconds after completion (default 120). `--no-watch` disables both automatic update mechanisms and serving verification, but not initial verification or explicit refresh. Legacy v1 and ordinary-server policies are unchanged. `hello` advertises `event-driven-native-reconciliation` and `serving-native-periodic-verification`. Online `views.status` and ordinary `status` expose the effective `watch_mode`, `watch_fallback`, `reconciliation_policy`, `last_verification` and `watch_events`. Native `Modify` notifications other than renames are dropped when the file's current metadata matches the trusted read evidence of the current publication and its recorded hidden status (Windows `FILE_NOTIFY_CHANGE_ATTRIBUTES`, `LAST_ACCESS` and duplicate-write churn). Creates, removes, renames, rescans and Git/ignore inputs are never filtered, and nothing is filtered while a replacement is being prepared. A same-size, same-timestamp content rewrite is left to periodic verification. `watch_events` reports cumulative `events`, `by_kind` (`access`, `create`, `modify`, `remove`, `other`, `error`), `unchanged_paths_filtered`, `invalidated_paths`, `full_invalidations` and `epoch_advances`, so watcher input churn can be measured directly. The policy reports `incremental_coverage: "unavailable"`, `change_journal: "not-integrated"` and `native_verification_availability: "serving-until-difference"`. Unless watching is disabled, `native_periodic_full_verification` and `periodic_full_verification` are true, `interval_ms` is the active interval, and `native_missed_changes` is `"repaired-by-periodic-verification"`. `full_verification_reason` is `"polling-update-mechanism"` for a polling view (or configured poll mode) and `"periodic-native-verification"` otherwise. With `--no-watch`, the periodic fields are false/null and `native_missed_changes` is `"requires-explicit-refresh"`. The configured `poll_interval_ms` is always visible; `configured_watch_mode` is `auto`, `poll` or `disabled`. `last_verification` has `outcome` and `age_ms`. The outcome is `full-pass` after attachment or any other completed full reconciliation; for a serving verification it is `unchanged`, `repaired`, `failed` or `cancelled`. `native_registration_mode` distinguishes `recursive-roots` from `per-directory`, and `native_watch_budget_per_view` reports the startup allocation. Per-view `watch_count` counts live subscriptions (zero after fallback or drain); namespace `reconciliation_policy.watch_count` sums those subscriptions across monitored views. Online `namespace.status` and `maintenance.status` distinguish configuration from effective per-view behavior: the same policy has `scope: "monitored-views"` plus `native_views`, `polling_views` and `disabled_views` counts. A namespace configured as `auto` can contain both healthy native views and fallback-polling views. Its `full_verification_reason` is `"polling-update-mechanism"` when at least one monitored view polls, and repair triggers then include `poll-interval`; otherwise they include `periodic-verification`. Use explicit refresh or `--no-index` when bytes newer than the last verification or processed event are required. `views.adaptive` takes `view`, `owner`, `expected_version`, `allocation_version` in an operation envelope. Adaptive policy evaluates the worktree's captured exact `HEAD`, not another branch tip. It uses bounded actual-content work, high/low watermarks, minimum absolute/percentage reduction, cooldown and attempt limits, then invokes the same migration engine. Fixed-pin participants block it. Untracked-only churn, unchanged targets, and ineffective checkout transformations do not repeatedly publish ineffective generations. An eligible decision and its exact target/input epoch are persisted with the logical attempt count. Transient worker or object-guard contention can defer that same operation within its deadline without consuming another adaptive attempt, reapplying cooldown, or selecting a later `HEAD`. ### Management method reference | Methods | Parameters / result purpose | | --- | --- | | `hello`, `namespace.status`, `maintenance.status` | `{}`; negotiated contract, accounting, native memory and bounded scheduler/recovery/collection diagnostics | | `lookup` | `{"root":}`; live versioned routing/ready state | | `views.status` | `{"id":}`; live view details | | `views.recover` | `{"id":}`; authoritative persisted current state | | `objects.page`, `objects.inspect` | `{"cursor":null-or-returned-cursor}` / `{"id":}` | | `objects.references`, `objects.eligibility` | `{"id":}`; reference reasons and current eligibility | | `storage.inspect`, `storage.inventory` | `{"cursor":null-or-returned-cursor}`; bounded catalog accounting or actual filesystem inventory, including unknown entries | | `cursors.close`, `storage.inventory.close` | The returned cursor object directly; release its bounded lifetime | | `collections.preview` | `{"cursor":null-or-returned-cursor}`; candidate/reason/accounting page, not deletion authority | | `owners.inspect`, `owners.reap` | `{"id":}`; current proof, or explicit positive-proof reaping | | `owners.page`, `operations.pending` | `{"after":null-or-last-id}`; bounded keyset pages, not a stable catalog snapshot | | `operations.inspect`, `operations.cancel` | `{"id":}`; durable receipt/current cancellation outcome | | `operations.lookup` | Complete operation token directly | | `operations.acknowledge` | `{"scope":,"through":N}`; terminal receipt acknowledgement floor | | `views.detach` | `{"owner":,"lease":}`; idempotent lease release and remaining reader/root drain state | | `metadata.start`, `collections.start` | Owner-scoped operation envelope; asynchronous live admission | | `metadata.run`, `collections.run`, `maintenance.recover` | Namespace-scoped envelope; authorized bounded offline work | | `stop-if-idle` | `{"token":}`; live atomic admission closure and all-category idle decision | Metadata requests are tagged by `method` and `params`: ```json {"method":"policy","params":{"expected_version":1,"policy":{}}} {"method":"allocation","params":{"expected_version":1,"allocation":{}}} {"method":"retain","params":{"object":""}} {"method":"release","params":{"reference":""}} ``` Replace the empty objects with complete validated policy/allocation objects. Retain creates an explicit persistent cache reference; release names that reference, not the object's original attach intent. Metadata changes and their original operation receipt commit together. ## Managed maintenance and durability Catalog listing/inspection/preview cursors preserve bounded snapshot ordering while the live catalog changes. They are instance-bound, expire explicitly, consume cursor budget and can be closed. Collection continuation is separate: it stores bounded traversal/member progress and **rechecks current** references, identity, policy, allocation, grace and OS locks at each eligibility/deletion boundary. Replaying a preview page cannot authorize deletion. For a collection operation, use: ```json { "policy_version": 1, "allocation_version": 1, "bounds": { "max_duration_ms": 200, "max_examined": 128, "max_removed": 16, "max_delete_bytes": 16777216, "max_pages": 16 }, "cursor": null } ``` Caller bounds may tighten, not enlarge, policy bounds. Persist the returned `next` cursor and use a new operation token for the next pass; retry the old token only to recover that pass's receipt. A one-page pass can resume an already selected object's members without spending its only page selecting it again. The managed daemon schedules background recovery and collection traversals with an exponential backoff (base `retry_ms`, up to 64x). A pass that reclaims or changes something resets the backoff, and the rest of that traversal continues at the base interval. A traversal that changes nothing keeps its current backoff on every continuation and doubles it on completion, so an idle namespace is paced rather than rescanned back to back. Lifecycle triggers still run the next pass at the base interval. Catalog transactions that change no rows do not invalidate workers' cached operation control. The scheduler itself polls every 100 ms, but each full tick reads the catalog (and re-verifies its anchors) several times per view. When consecutive full ticks find nothing to do, ticks back off exponentially to at most every 1.6 seconds (`scheduler.idle_tick_ms`). Any change to the in-memory pacing signature (a lifecycle trigger, a view input epoch from a watcher event or refresh, an attached or detached view, or a job finishing) runs the next full tick immediately. Other changes, such as a lease release on an otherwise idle namespace, may be noticed up to one idle tick later. This trades a bounded scheduling lag for less catalog I/O competing with running reconciliation. Checkpoints are delta-only, bound to exact namespace, repository, root identity, view/version, base key/incarnation/fingerprint and processed epoch. Publication atomically updates the current binding. Collection withdraws a checkpoint before releasing its generation edge. Generation/checkpoint lifecycle is: `Published -> Retired -> PendingDeletion -> Removed`. New references cannot resurrect a retired physical incarnation. Active views, migration/build inputs, live old readers, restores and persistent retains protect their objects. Creation intent precedes filesystem creation. Writers are not sealed merely because a file was synced; open/unfinished producers cannot be published. An interrupted producer can leave payload bytes with only a prefix of durable proof. Such unsealed or unexpectedly changed objects remain quarantined and conservatively charged; proving producer death alone does not make missing content proof trustworthy. Recovery may validate an already complete intended seal, but never synthesizes one from the abandoned bytes. Deletion journals exact owned identities and each bounded physical step before performing it, then records the result. Authentication-block-aligned tail truncations and final unlink cannot exceed the remaining deletion-byte budget. After interruption, the exact journaled target length alone is not success: recovery verifies every surviving prefix block against the original producer proof before crediting the removed tail. A rewritten prefix at that same length is preserved with no recovered credit. A missing member is recoverable only from the corresponding authenticated unlink intent, not from an existing zero-length file or a generic missing-file error. Reclaimed logical/allocated bytes are credited only from verified physical results. Sharing violations, retained Windows mappings, permission failures, unknown owners and missing evidence remain explicit skips/errors, not fake successful reclamation. Retrying uses a fresh bounded pass. Content verification is separately bounded work, not reclaimed space. A pass uses `max_delete_bytes` as a separate verification-byte allowance, with proof pages also subject to page, duration, cancellation and allocation checks. Consequently, deletion-byte limits are not a claim about total physical I/O. A member larger than one pass can resume verification using a bounded, volatile context; verification bytes, invalidations and retained resources are reported separately. Restart, cancellation, expiration or loss of valid native evidence discards that context. Policy/allocation version changes also discard all cached proof, but can keep a still-valid native guard for the same member while restarting authentication at the first byte. Durable deletion intents do not preserve authority over an earlier verified prefix. Read-only inventory duplicates a matching cached descriptor rather than breaking its native lease with a new open; an active verifier can produce a typed retryable busy finding. One short-lived control-file verifier per namespace holds an OS-backed activity guard. Open admission checks its exact directory/member identity. A competing cleaner, inventory pass or owner probe for that member reports retryable contention rather than attempting a new open that would break a Linux lease. Other control members remain accessible. This is not a persistent namespace-wide reader pin: unrelated ready views continue serving, and maintenance can retry after the bounded control operation ends. If this admission conflict interrupts a collection unit, its typed Busy detail is reported as a skip, not a failed deletion. The continuation retains that exact object, including any already credited member removals; it never reports the object's removal complete until its remaining control cleanup finishes. Native evidence is checked around each verification page and immediately before destructive I/O: - Windows retains the same deny-write/delete-sharing handle through verification and the destructive unit. Conflicting handles or retained writable mappings leave explicit pending work rather than weakening the sharing mode. - Linux uses an exclusive native file lease where supported. Break requests or forced revocation invalidate the context; an old successful acquisition is not proof that the lease is still held. - macOS uses a supported, nonzero native content generation and pinned-vnode notifications. Missing evidence or a writable-mapped/zero-generation state prevents collection. These are change observations, not writer exclusion. The context is discarded after each tgrep truncation too: adopting its new generation could otherwise hide an interleaved foreign prefix write. The remaining prefix is authenticated again over subsequent bounded passes. Identity, ownership and actual length are rechecked around physical work. Detected interference or uncertainty is reported, not adopted as a new trusted seal for the next unit. Reverification can add substantial read work even when few bytes are deleted; diagnostics and measurements must include that cost. Grace is conservatively re-established after restart, clock rollback or missing age evidence. Byte targets are best effort under safety constraints; shortfalls include protected/unknown/retry reasons. Passes bound examined objects, successful removals, deletion bytes, pages and elapsed work, including partial-file continuations. In-flight OS calls may overrun a deadline; progress reports elapsed time and budget exhaustion rather than claiming hard real-time preemption. A pass with no errors can therefore leave an object `PendingDeletion` after consuming a bound, even after a conflicting native handle has closed. Preserve its actual and recovered credits and continue from the returned cursor with a fresh operation token and current policy/allocation versions. Require the final `Removed` state and complete accounting before claiming the object was reclaimed. ```bash tgrep shared discover /trusted/cache tgrep shared maintenance /trusted/cache/tgrep-managed-v2/ hello tgrep shared maintenance /trusted/cache/tgrep-managed-v2/ collections.preview tgrep shared maintenance /trusted/cache/tgrep-managed-v2/ session --apply ``` Discovery streams bounded NDJSON pages. Maintenance opens storage directly, without Git or a surviving repository, under the same exclusive ownership lock as startup. Without `--apply` it cannot retire/delete objects or release references. With `--apply`, `metadata.run`, `collections.run` and `maintenance.recover` use persisted **namespace-scoped** operation tokens; live asynchronous `.start` is unavailable offline. Recovery requests are `{"cursor":null-or-returned-recovery-cursor}` and use policy bounds. Concurrent replay preserves the first completed, failed or cancelled recovery receipt. Newly eligible work requires a new token, not reuse of that receipt. A maintenance `session` accepts NDJSON `{"id":1,"method":"objects.page","params":{"cursor":null}}` and keeps one ownership/instance lifetime through EOF. Use it for cursor continuations; separate CLI invocations create different instances. Each reply has the caller's ID and an explicit `ok` result/error. It is not a background daemon and does not implicitly recreate a deleted repository. SQLite WAL transactions use FULL synchronization. Data is synced before catalog publication and directory entries are synced where the platform supports it. `hello.directory_sync` explicitly reports the Windows limitation; process-crash recovery is not a claim that unsupported directory flushes become power-loss guarantees. Recovery validates identities and sealed state before serving. On POSIX, catalog identity/accounting probes use no-open metadata observation: closing an independently opened database/sidecar descriptor can otherwise release SQLite's process-wide record locks. WAL truncation is attempted without waiting before catalog mutations, never after a successful commit. A retained SQLite snapshot does not reject indexed queries or lifecycle work while publication headroom remains. After acquiring the actual SQLite writer lock, every managed writer rechecks policy and physical lengths and reserves a complete publication: database growth, WAL headers and frames, worst-case FULL-sync sector padding (including VFSes without powersafe overwrite), and future WAL-index regions. Genuine exhausted headroom is a retryable, pre-mutation error; a durable WAL commit does not become a failed publication merely because truncation would have to wait for a reader. Within `metadata_bytes`, database page payload is capped at one third. SQLite files, including WAL, shared memory and any retained rollback journal, use at most five sixths; the remaining sixth covers bounded registered control files and the namespace header. New-work admission also checks observed total metadata. Externally enlarged anchors may exceed that target but do not prohibit bounded, reference-safe cleanup. Policy reductions must fit both existing pages and their own worst-case publication before commitment; a rejected reduction leaves the prior policy available for cleanup or a version-checked increase. All managed connections disable automatic checkpoints, page-cache spill and database mmap. The spill-disabled page payload bound is **per catalog connection**, with only one SQLite writer per namespace; the 256 KiB clean-cache setting is a soft target, not a hard memory limit. `maintenance.status` reports the latest primary writer's page/payload bounds, checkpoint result and observed native cache bytes before commit. Native cache bytes include pager headers but exclude other connections and non-pager SQLite allocations. These control-plane observations are separate from build-buffer reservations and are not aggregate RSS or process-heap limits. All managed failures have `category`, stable `reason_code`, `retryable`, `committed_state` (`not-committed`, `committed`, `unknown`) and local `detail`; operation ID/current version accompany relevant failures. Categories distinguish invalid input, busy, stale identity/version, evicted/missing cache, unavailable Git objects, incompatible/corrupt metadata, resource pressure, permission/I/O, cancelled/deadline, expired receipts and recovery-required. A transport failure after request transmission has unknown commitment, not automatic retry-as-new authorization. Recover the retained token and exact current state. `stop-if-idle` first closes admission, then accounts for leases, owners, reservations, operations and receipt readers, independent namespace/root/object guards, requests, queries, queued jobs and background batches. Idle verifier caches are discarded only when those other shutdown blockers are clear; a busy probe preserves bounded member-verification progress until its normal expiry. Active verification remains protected by its work lifetime. A retained cache alone does not make a daemon permanently busy. A busy decision reopens admission; a committed stop leaves it closed before exit. An uncommitted lock-admission Busy error leaves the accepted token pending; retry that same token rather than abandoning it and consuming another queue slot. A completed non-stopping decision is terminal and replayable, including concurrent uses of that token; a later idle probe needs a new token. Disconnecting the final client or checking the lease count alone is not atomic shutdown. Committed operation acceptance or bookkeeping alone does not authorize exit. Error recovery checks the current instance's durable stop decision together with closed admission; a busy or uncommitted decision restores admission. `maintenance.status` exposes bounded per-instance aggregate counters separately from free-form last-error diagnostics. Live status also reports native resident and private-memory observations, including whether a peak is OS-provided or only a sampled lower bound. Unsupported measurements are tagged unavailable, not zero. See [qualification commands](CONTRIBUTING.md#managed-lifecycle-qualification) and the [managed measurement protocol](SHARED_INDEX_BENCHMARKS.md#managed-lifecycle-measurements) for exact workloads and interpretation.