Commit graph

1,824 commits

Author SHA1 Message Date
trvon
c766a17bd1
Merge branch 'refactor/remove-symbol-abi' into release/v0.20-integration
# Conflicts:
#	tests/scripts/portability_allowlist.txt
2026-09-22 15:14:55 -06:00
trvon
66689cd7a3
refactor(daemon): remove the symbol_concurrent tuning knob and stage metrics
The retired symbol stage never activates, so tuning.post_ingest.
symbol_concurrent and YAMS_POST_SYMBOL_CONCURRENT had no effect, and
status reported symbol_* queue metrics and post_symbol_limit that were
always zero. Remove the config key and environment override, the status
keys and CLI rows, the metrics snapshot fields, and the event-bus symbol
counters. The internal stage slot stays so budget and limiter indices
are unchanged; benchmark JSON keeps its symbol fields at zero.
2026-09-22 14:41:05 -06:00
trvon
0707331a8d
perf(graph): stop loading document content for completion-only jobs
Since v0.20 the graph stage only records knowledge-graph completion
(NL entities are written by the post-ingest title+NL stage), yet it
still read every supported document from the content store and copied
it into a job that ignored it, and logged each one at info level. Queue
the completion job without content.

The isolated-node graph query now defaults to the semantic_neighbor
relation instead of the removed calls edges, so it reports documents
with no embedding neighbours.
2026-09-22 14:41:00 -06:00
trvon
ac070d8d2a
refactor(search): remove symbol-based result ranking
SymbolEnricher boosted results by looking up code-symbol nodes that no
longer exist since v0.20, so it ran KG queries for the top results and
never boosted anything. Remove it, the YAMS_SYMBOL_WEIGHT variable, the
symbol_rank_* search stats, and the symbolRank option on the service,
retrieval, MCP and CLI request types. The IPC field stays in the wire
format but is ignored, and --symbol-rank/--no-symbol-rank remain
accepted as hidden no-ops so existing scripts keep working.
2026-09-22 14:40:52 -06:00
trvon
25d171dd62
ci(fuzz): run the fuzz lane on the self-hosted Forgejo runner
Fuzzing belongs on our own compute rather than GitHub-hosted runners.
Move the smoke job to .forgejo/workflows on the self-hosted label, pin
actions to code.forgejo.org, and add a preflight that fails clearly when
the runner lacks clang's libFuzzer runtime or the build tools.

Note that once .forgejo/workflows exists, Forgejo runs only those
workflows and stops executing the .github/workflows copies.
2026-09-22 14:17:16 -06:00
trvon
c6e960eed7
Merge branch 'test/p2p-fuzz-lane' into release/v0.20-integration 2026-09-22 13:55:37 -06:00
trvon
e6de1978c2
Merge branch 'refactor/remove-symbol-abi' into release/v0.20-integration 2026-09-22 13:55:37 -06:00
trvon
34b9f7346e
docs(p2p): describe the fuzz lane, its oracles and remaining gaps 2026-09-22 13:55:12 -06:00
trvon
6065798e65
ci(fuzz): run the P2P and topology harnesses on relevant changes
Build the libFuzzer targets with clang through setup.sh Fuzzing and run
each for 60 seconds over the generated seeds on pull requests touching
the P2P, codec or fuzzing sources, nightly, and on demand. Reproducers
are uploaded when a harness fails.
2026-09-22 13:55:10 -06:00
trvon
2a63eb1af8
test(fuzz): add a direct-P2P fuzz lane and a topology codec harness
New libFuzzer/AFL++ harnesses, each trapping on an oracle violation:
p2p_json (nesting bound, round trip), p2p_connstr (endpoint round trip,
pin idempotence, no silent re-pin), p2p_handshake (validators and the
full acceptor bind the TLS peer), p2p_delta_stream (batch, session and
bootstrap receive paths stay inside count and byte budgets), p2p_inbound
(the manager inbound session through registry update) and
topology_codec (decoded snapshots re-encode losslessly). p2p_delta now
draws its exchange limits from the input.

p2p_seeds.py writes framed-session seeds for FuzzedDataProvider layouts,
dicts/p2p.dict carries the frame vocabulary, generate_corpus.sh accepts a
local seedgen, and fuzz.sh no longer fails on macOS bash 3.2 when a
target has no dictionary. The generated corpus stays untracked.
2026-09-22 13:55:04 -06:00
trvon
b702cb47e1
fix(p2p): validate connection-string hosts
Found by fuzz_p2p_connstr. Hosts containing brackets, spaces, control
or non-ASCII bytes were accepted and could not be rendered back through
endpoint(). Accept only hostname/IPv4 characters for plain hosts and
hex, ':', '.' plus an optional %zone inside brackets.
2026-09-22 13:53:10 -06:00
trvon
de9e3245f8
fix(p2p): admit only non-negative integers in control frames
Found by fuzz_p2p_handshake. A pre-authentication hello whose integer
field exceeded uint64 was stored by the JSON parser as a double and then
read with get<std::uint32_t>(), an undefined float-to-integer
conversion. A value just above 2^32 was also silently truncated, so
protocol 2^32+4 passed the protocol-4 version check, and negative values
wrapped to huge sizes.

Reject any frame containing a float or a negative number at the frame
boundary (the wire carries none), which also keeps the memory-sync
record deserializers defined, and read p2p numeric fields through a
range-checked unsignedField<T>.
2026-09-22 13:53:04 -06:00
trvon
dc8dc76ce3
refactor(p2p): run handshake and delta exchange over a FrameSource
The handshake, delta-exchange and inbound-session state machines read
frames straight from the TLS P2pConnection, so none of the multi-frame
parsing or payload accounting could be exercised without a socket.
Introduce a private FrameSource seam with a production adapter over
P2pConnection and an in-memory BufferFrameSource, pass TLS-derived
identity values explicitly, and expose the validators and receive paths
in p2p_fuzz.h. Behaviour is unchanged; the transport, protocol, delta,
registry and manager suites pass.
2026-09-22 13:34:36 -06:00
trvon
5cf6e7119f
build(fuzz): build fuzzers with plain clang and libFuzzer
The fuzzing subdirectory skipped itself unless the compiler was AFL++,
so fuzzers could only be built inside the Docker image and no quick
local or CI run was possible. With -Dbuild-fuzzers=true a clang that
links the libFuzzer runtime now selects libFuzzer: project objects get
-fsanitize=fuzzer-no-link plus ASan and UBSan unless b_sanitize is set.
AFL++ builds keep their previous flags.

fuzz.sh gains a 'local' command that runs a libFuzzer target over its
seed corpus and passes a dictionary when tools/fuzzing/dicts has one.
2026-09-22 13:34:20 -06:00
trvon
ac60e84f57
fix(build): include <algorithm> where std algorithms are used
db_salvage.cpp and the SQLite KG store used std algorithms through
transitive includes that a plain clang libFuzzer build does not
provide.
2026-09-22 13:34:14 -06:00
trvon
8faa5f43c3
docs: describe the graph without code-symbol nodes
README, AGENTS.md, the yams skill guide and the engineering prompt
still promised symbol extraction, calls/defined_in edges, and graph
--impact for callers. Point callers and blast radius at yams grep and
describe the relations the graph still holds.
2026-09-22 13:30:52 -06:00
trvon
ec5b78980e
refactor(graph): resolve explore through graph nodes and report removed impact
Graph explore looked up entry points in symbol_metadata, which no longer
exists. Resolve them from node labels, aliases, and ingest path nodes
instead, gather a wider candidate pool, and rank it by query-term
coverage per file before applying the symbol budget so broad early
terms cannot crowd out the file that answers the query.

Impact and affected-test analysis walked code-symbol edges that are no
longer produced; they now return a warning saying the symbol graph was
removed in v0.20 instead of silently empty results.
2026-09-22 13:30:46 -06:00
trvon
c39ed84bf7
refactor(plugins)!: remove the symbol_extractor_v1 plugin ABI
Migration 40 dropped the symbol tables and graph edges, but the daemon
could still adopt an external symbol_extractor_v1 plugin (for example a
0.19 plugin left in a user plugin directory) and rebuild them.

Remove the interface header, the ABI adapter, plugin adoption, the
EntityGraphService extract-and-write path, the post-ingest symbol
channel, the write-coordinator symbol ops, and the KG-store symbol
metadata and extraction-state API. The ABI loader and the external host
now refuse a plugin that declares the interface (or the legacy
symbol_extraction capability) with a one-time warning. Status no longer
reports symbol extractors, generated configs no longer write
[plugins.symbol_extraction] (an existing section still parses), and
the benchmark suite drops symbol_extraction_bench.

Tests that exercised the dropped tables or the v39 schema template are
removed; the rest run against the current schema.

BREAKING CHANGE: code-symbol extraction is gone. Removed: the
symbol_extractor_v1 plugin ABI, `yams config grammar` (list, download,
path, auto-enable, auto-disable), the grammar steps in `yams init`, the
meson option `plugin-symbols`, the conan option
`enable_symbol_extraction`, and the Windows installer SymbolPlugin
component. Databases migrate to schema 41; a v0.19 binary refuses to
open them, and symbol data dropped by migration 40 cannot be recovered.
2026-09-22 13:30:35 -06:00
trvon
8912d70e5c
perf(bench): expose BQ routing work and new topology levers to benchmarks
Report topology_route_bq_used / topology_route_bq_candidates in search
debug stats so benchmarks can tell BQ-shortlisted routing apart from
ANN and exhaustive routing. Let retrieval_quality_bench write
bq_candidate_limit, bq_prefix_dim, sgc_hops and
simeon_rerank_outer_maxsim into its per-run config from YAMS_BENCH_*
harness settings, read through getenv_optional.
2026-09-22 12:51:12 -06:00
trvon
4bc65fe3b8
perf(topology): emit profiling zones from topology and vector code
src/topology and src/vector were compiled without TRACY_ENABLE even in
-Denable-profiling builds, so their YAMS_* zones were no-ops. Mirror the
src/search setup in both libraries, then add zones for snapshot
encode/decode, store and load, SGC smoothing, route-index build and
routing, BQ search, the snapshot cache, and outer MaxSim, plus plots for
snapshot payload bytes, membership count and BQ index bytes.
2026-09-22 12:37:44 -06:00
trvon
7eb64fa8a2
test(search): include the BQ index header in the snapshot cache test
The BQ prefix assertions dereference centroidBqIndex, which the routing
headers only forward-declare.
2026-09-22 12:37:38 -06:00
Trevon
af6f89fc6e
Merge pull request #181 from trvon/ci/release-promotion-safeguards
Some checks failed
Secret scan / gitleaks (push) Failing after 9s
Mirror source to Forgejo / mirror (push) Has been skipped
Release Candidate / Validate promotion and preview release (push) Has been cancelled
Tests / Static Analysis (push) Has been cancelled
Tests / Warm Conan & ccache (linux-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (linux-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (windows-x86_64) (push) Has been cancelled
Tests / warm (push) Has been cancelled
Tests / Tests (linux-arm64) (push) Has been cancelled
Tests / Tests (macos-arm64) (push) Has been cancelled
Tests / Tests (linux-x86_64-asan) (push) Has been cancelled
Tests / Tests (linux-x86_64-ubsan) (push) Has been cancelled
Tests / Tests (linux-x86_64) (push) Has been cancelled
Tests / Tests (windows-x86_64) (push) Has been cancelled
ci(release): decouple draft promotion and enforce non-squash merge policy
2026-09-22 12:35:20 -06:00
trvon
715a0428df
fix(rerank): sample MaxSim fragments across the whole document
Outer MaxSim scored only the first eight sentences of each document, so
a relevant passage later in a long document was never seen, which
defeats the point of fragment scoring. Sample sentences evenly across
the document (first and last kept), always reserve a document-head
fragment, and bound per-call fragment embeddings so large rerank
windows shrink the per-document budget instead of growing unbounded.
2026-09-22 12:31:45 -06:00
trvon
c990b4878d
fix(topology): make SGC smoothing deterministic and skip ragged inputs
Edges were emitted in unordered_map iteration order, so float
accumulation, and the smoothed embeddings feeding snapshot fingerprints,
varied with input order. Order each neighbor list by document hash.

Documents whose embedding is missing or of a different dimension were
treated as zero vectors yet still added degree, damping their
neighbors. Exclude them from the propagation graph. Hop 0 now reads the
inputs directly, dropping one n*dim buffer for single-hop smoothing.

Add closed-form checks against D^-1/2 (A+I) D^-1/2 for one and two
hops.
2026-09-22 12:28:34 -06:00
trvon
b9ec471c4c
feat(topology): wire SGC smoothing and BQ routing through typed config
sgcHops, sgcNormalize, bqCandidateLimit and bqPrefixDimension existed
only as struct fields, so no production path could enable them. Resolve
topology.sgc_hops / topology.sgc_normalize into the TopologyManager
build config (hops capped at 4 to bound over-smoothing) and
search.topology.bq_candidate_limit / bq_prefix_dim into the routing
options, request, and fingerprint. The snapshot cache rebuilds only the
route index when the BQ prefix changes. All default to off.

Drop the snapshot interner, which was allocated but never used.
2026-09-22 12:25:47 -06:00
trvon
da3e398b5c
fix(topology): keep exhaustive routing when BQ cannot score a query
The BQ branch cleared every route candidate before searching, so a
query the index could not quantize (shorter than the indexed dimension)
left only sparse-seeded clusters. The BQ fallback runs whenever the
dense ANN index is absent, which made this reachable in production.
Only narrow candidates when BQ returns a shortlist, matching the dense
ANN branch, and document that the SimHash similarity estimate is an
ordering proxy for raw sign bits.
2026-09-22 12:20:21 -06:00
trvon
438f8a8a7b
ci(release): refuse stable releases from non-version refs
A stable workflow_dispatch took the release tag from GITHUB_REF_NAME
without checking it, so dispatching from main published a release
tagged main. Require refs/tags/v* or refs/tags/yams-v*.

Fetch main and experimental with explicit refspecs in the candidate
preflight so a tag named main cannot shadow the branch, and drop the
workflow_run trust path from draft reconciliation now that the workflow
is dispatch-only.
2026-09-22 12:18:20 -06:00
trvon
c401e045d0
fix(metadata): guard snapshot pruning and topology count triggers
Migration 40 pruned every topology snapshot when the latest pointer was
missing or named a snapshot that did not exist, and aborted on malformed
pointer JSON. Prune only when the pointer is valid JSON naming an
existing snapshot node.

Migration 41 matched topology keys with LIKE, which is case-insensitive
in SQLite and also stopped counting user keys such as Topology.*. Use
GLOB, and rebuild the topology counts on rollback so down leaves
metadata_value_counts consistent. Neither migration has shipped in a
release or nightly.
2026-09-22 12:15:41 -06:00
trvon
fff24242fd
fix(topology): make the latest pointer the storeBatch commit point
storeBatch wrote per-document keys before the snapshot node, so a
failure in between left documents pointing at a snapshot that was never
persisted. Write the snapshot node first and the latest pointer last so
a partial store leaves the previous snapshot authoritative.

Remove only the two per-document keys still written, in one transaction
via a new removeMetadataBatch, and stop the membership slow path from
reading structural keys that migration 41 prunes.
2026-09-22 12:12:43 -06:00
trvon
522c33a9ca
fix(topology): share one resident snapshot behind a lock
The metadata store cached the latest batch in an unguarded mutable
optional that search threads read and wrote, and handed out a full copy
on every loadLatest, so search kept the snapshot resident twice.

Publish the latest batch and its membership index as one immutable
shared snapshot under a mutex, add loadLatestShared to hand it out
without copying, and feed the routing snapshot cache from it. Historical
loads no longer displace the resident latest snapshot, and a reader that
raced a write never installs the older batch.
2026-09-22 12:05:53 -06:00
trvon
5267b32775
fix(topology): bound snapshot decode against corrupt payloads
Validate every element count against the remaining payload before
reserving, reject trailing bytes, type-check envelope fields, bound the
untrusted uncompressed_bytes hint, read the magic little-endian, and
convert legacy JSON type errors into Result errors instead of throwing.
2026-09-22 11:59:44 -06:00
trvon
8a72da96ff
test(metadata): align legacy symbol test cases with schema v39
- Use v39 metadata DB template for legacy symbol metadata test cases in
  metadata_schema_catch2_test.cpp and kg_store_catch2_test.cpp to avoid
  running against schema v40 where symbol tables are dropped.
- Use yams::test::make_temp_sqlite_path instead of raw getenv in
  metadata_schema_catch2_test.cpp and prune the stale portability allowlist entry.
- Add site directory to SKIP_PARTS in check_repository_metadata.py to prevent
  local docs builds from triggering retired marker warnings.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 09:37:00 -06:00
trvon
2bfc5334eb
ci(release): decouple draft promotion and enforce non-squash merge policy
- Decouple draft-promotion.yml from automated Tests workflow_run triggers,
  making promotion draft creation manual via workflow_dispatch to prevent
  GitHub from auto-chaining active experimental feature PRs into a stack
  targeting main.
- Enforce and document the mandatory Promotion Merge Policy across
  reconcile_promotion_pr.py, validate_release_candidate.py, and draft-promotion.yml
  step summary: promotions must use a merge commit (--no-ff) or fast-forward,
  never squash-merge into main, preserving conventional commit history and
  unbroken git ancestry.
- Unhide refactor commits in release-please-config.json so architectural
  refactorings are tracked under Changed in CHANGELOG.md.
- Update unit tests in test_reconcile_promotion_pr.py and
  test_validate_release_candidate.py for the new policy invariants.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:58:31 -06:00
Trevon
3f3b94f972
Merge pull request #175 from trvon/perf/topology-embedding-memory
Some checks failed
Secret scan / gitleaks (push) Failing after 8s
Mirror source to Forgejo / mirror (push) Has been skipped
Release Candidate / Validate promotion and preview release (push) Has been cancelled
Tests / Static Analysis (push) Has been cancelled
Tests / Warm Conan & ccache (linux-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (linux-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (windows-x86_64) (push) Has been cancelled
Tests / warm (push) Has been cancelled
Tests / Tests (linux-arm64) (push) Has been cancelled
Tests / Tests (macos-arm64) (push) Has been cancelled
Tests / Tests (linux-x86_64-asan) (push) Has been cancelled
Tests / Tests (linux-x86_64-ubsan) (push) Has been cancelled
Tests / Tests (linux-x86_64) (push) Has been cancelled
Tests / Tests (windows-x86_64) (push) Has been cancelled
perf(topology): binary snapshot compression and BQ routing
2026-09-22 07:47:59 -06:00
trvon
c3b667ee40
feat(retrieval): SGC topology smoothing, Matryoshka BQ prefix, and fragment outer-MaxSim
Implement three research-backed retrieval optimizations connecting
paperbridge literature findings with YAMS subsystems:

1. SGC Feature Smoothing (Proposal A):
   - Add sgcHops and sgcNormalize to TopologyBuildConfig.
   - Diffuse document embeddings across reciprocal nearest-neighbor graph
     edges prior to clustering, formalizing Lean SGC.lean.
   - Re-normalize diffused embeddings to preserve unit hypersphere metric geometry.
   - Wire into ConnectedComponent, Louvain, and KMeans topology engines.

2. Matryoshka Prefix Binary Quantization (Proposal C):
   - Add maxDimensions parameter to BinaryQuantizer::quantize and
     maxPrefixDimension to BinaryQuantizedIndex::build.
   - Support fast 64-bit word-aligned prefix Hamming shortlisting in
     SparseGuidedClusterRouter via bqPrefixDimension.

3. Fragment Outer-MaxSim Reranker (Proposal B):
   - Implement SimeonScoringMode { SingleVectorCosine, FragmentOuterMaxSim }
     in SimeonModelProvider.
   - Add extractDocumentFragments and late-interaction outer-MaxSim scoring
     inspired by ColBERTv2/PLAID/SPLATE to prevent relevance dilution in long docs.
   - Expose simeonRerankOuterMaxSim on SearchEngineConfig and ConfigResolver.
   - Add Catch2 test coverage and verify BEIR scifact benchmark metrics.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:35 -06:00
trvon
052031a484
feat(vector): binary quantization (BQ) pre-filter for cluster routing
- Add BinaryQuantizer and BinaryQuantizedIndex in include/yams/vector/binary_quantization.h
  and src/vector/binary_quantization.cpp implementing 1-bit sign quantization,
  packed 64-bit word storage, hardware popcount Hamming distance, and cosine
  similarity estimation.
- Integrate centroidBqIndex into SparseRouteIndex, constructing compact BQ
  centroids alongside dense ANN indices during SparseGuidedClusterRouter::buildRouteIndex.
- Add bqCandidateLimit to TopologyRouteRequest and tracking metrics (bqUsed,
  bqCandidates, bqDistanceEvaluations) to SparseRouteWork.
- Enable fast candidate pre-filtering in SparseGuidedClusterRouter::route with
  1-bit Hamming distance scans, providing sub-millisecond coarse candidate
  shortlisting and automatic fallback when the dense ANN index is omitted.
- Add Catch2 unit test suites in tests/unit/vector/binary_quantization_catch2_test.cpp
  and tests/unit/topology/topology_baseline_catch2_test.cpp.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:35 -06:00
trvon
1550dad3be
perf(search): flyweight string interning in TopologyRoutingSnapshotCache
- Add StringInterner in include/yams/search/string_interner.h for
  reusable, thread-compatible flyweight string deduplication with
  heterogeneous string_view lookups.
- Update TopologyRoutingSnapshot in include/yams/search/topology_routing_session.h
  to store zero-copy string_view keys for clustersById and membershipsByDocumentHash,
  pointing directly into immutable TopologyArtifactBatch strings.
- Add shared_ptr<StringInterner> to TopologyRoutingSnapshot to intern repeated
  lookup and dynamically generated strings.
- Eliminate ~75,000 duplicate heap-allocated std::string objects per cached
  snapshot, reducing cache footprint by ~70%.
- Add Catch2 tests in tests/unit/search/string_interner_catch2_test.cpp covering
  interning, deduplication, lifetime guarantees, and zero-copy pointer preservation
  across snapshot upgrades.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:35 -06:00
trvon
dd8105f25d
perf(topology): bypass metadata table triggers during snapshot sync and prune legacy metadata
- Add Migration 41 to drop and recreate metadata_value_counts triggers with
  'WHEN ... AND key NOT LIKE topology.%' condition, eliminating ~650,000 trigger
  executions per topology rebuild in a 72k document corpus.
- Purge legacy continuous float/structural keys from metadata table
  (persistence_score, cohesion_score, bridge_score, role, cluster_level,
  parent_cluster_id, overlap_cluster_ids_json) in Migration 41.
- In MetadataKgTopologyArtifactStore::storeBatch, replace N individual
  getDocumentByHash calls with single batchGetDocumentsByHash and write
  only essential snapshot_id and cluster_id to metadata table.
- In MetadataKgTopologyArtifactStore::loadMemberships, add in-memory fast
  path serving memberships directly from cached latest snapshot batch.
- Update serializeTopologyBatchCompressed to handle uncompressed tiny batches.
- Add Catch2 unit tests in tests/unit/topology/topology_metadata_store_catch2_test.cpp
  validating trigger bypass, metadata pruning, and fast-path membership loading.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:34 -06:00
trvon
8fdb4df27e
feat(topology): binary snapshot serialization and zstd compression
Replace 960+ MB raw JSON snapshot serialization in MetadataKgTopologyArtifactStore with a compact, string-interned binary format compressed with Zstandard.

- Add yams::topology::serializeTopologyBatchBinary and deserializeTopologyBatchBinary with integer length packing and string deduplication.
- Wrap binary payload in a backward-compatible JSON envelope with format: zstd_binary_v1 and base64 encoded payload, maintaining SQLite json_valid(properties) == 1.
- Fall back gracefully to legacy raw JSON deserializer when reading pre-existing uncompressed snapshots.
- Add Catch2 unit test suite verifying binary roundtrip fidelity, compression ratio, backward-compatibility with legacy JSON snapshots, and corrupted payload resilience.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:34 -06:00
trvon
32e57d758d
refactor(symbols): remove tree-sitter from conan and fix legacy symbol tests
- Remove enable_symbol_extraction option and tree-sitter requirement from conanfile.py.
- Remove YAMS_DISABLE_SYMBOL_EXTRACTION toggles from setup.ps1, setup.sh, and ubuntu.Dockerfile.
- Add v39_metadata_db_template helper to MetadataDbTemplate for legacy symbol tests.
- Update GraphComponent, DeleteCascade, GraphContextService, and KGStore test
  fixtures to run symbol-specific test cases against schema v39.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:30 -06:00
trvon
ee51abcb1a
refactor(symbols): remove in-tree tree-sitter symbol extractor plugin and tests
Remove legacy Tree-sitter symbol extractor plugin (plugins/symbol_extractor_treesitter),
the unused plugin-symbols build option, and associated tests and benchmark targets:
- Drop plugins/symbol_extractor_treesitter/ (grammar_loader, symbol_extractor, plugin_api).
- Remove plugin-symbols Meson option and packaging references.
- Remove dedicated symbol plugin tests and benchmark targets (symbol_extraction_bench,
  symbol_extractor_all_langs_catch2_test, symbol_extractor_edge_cases_catch2_test,
  symbol_extractor_solidity_catch2_test, symbol_extractor_query_test,
  symbol_extractor_validation_test).
- Clean up dead symbol extraction plugin helper and test from hnsw_entity_search_bench.
- Prune obsolete portability allowlist entries.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:30 -06:00
trvon
9c7f780d4c
refactor(symbols): remove symbol extraction subsystem and add migration 40
Drop legacy Tree-sitter symbol extraction subsystem, associated database
tables, triggers, and AST edge types:
- Migration 40 drops symbol_metadata, document_symbol_extraction_state,
  symbol_metadata_fts, associated triggers, and prunes historical bloated
  topology snapshots while preserving active snapshot topology,
  semantic_neighbor edges, and non-symbol observed_as provenance edges.
  Exclude nl_entity:% nodes from kg_nodes / kg_edges pruning to preserve
  canonicalized method-type technology entities.
  Provides complete downSQL restoring symbol tables, indexes, FTS virtual table,
  and sync triggers for clean v39 rollbacks.
- In EntityGraphService, ensure locally resolved documentDbId propagates to
  WriteBatch for knowledgeGraphDocumentId to ensure accurate enrichment
  token completion, and only populate batch->documentIdToDelete for snapshot
  documents with a resolved document database ID.
- In SymbolEnricher and SearchService, restore one-directional symbol-contains-query
  matching with non-empty checks to prevent short substrings matching broad queries.
- In tests/unit/metadata/symbol_metadata_schema_test.cpp, migrate test DB up to
  version 39 so tests pass against their target version prior to v40 table drops.
- Remove Tree-sitter grammar download and compile workflows from CLI init
- Default plugin-symbols to false.
- Fix macOS daemon client and CLI executable resolution using
  _NSGetExecutablePath with candidate fallbacks.
- Add migration validation tests and symbol enrichment unit tests.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:30 -06:00
trvon
fb5270e5a9
fix(bus): explicitly include <algorithm> for std::erase_if
Add missing #include <algorithm> in include/yams/daemon/components/InternalEventBus.h
to ensure std::erase_if is declared across all standard library implementations
without relying on transitive includes.

Addresses Copilot review finding on PR #173.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:30 -06:00
trvon
cfaff9480e
fix(bus): keep the channel-registry mutation off the production API
remove_channel() was added as a test helper but left publicly callable in every build. Existing
holders keep their shared_ptr, so calling it while producers or consumers are live creates two live
queues for one name and silently splits or drops events between the old holders and the next
get_or_create_channel(). Copilot raised this on #170.

Gate remove_channel() and the test-facing capacity_mismatches() accessor behind YAMS_TESTING,
matching the convention already used by other production headers (retrieval_service.h,
memory_sync.h, mcp_server.h). The production-facing part of the diagnostic - the warn emitted from
get_or_create_channel - stays enabled in every build; only the registry mutation and the query
accessor become test-only, and neither has a caller outside the test tree.

Verified: grep finds callers only in tests/unit/daemon/daemon_background_processing_test.cpp; the
production daemon library compiles without YAMS_TESTING; the test binary is 307/307.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:30 -06:00
trvon
b6f5faba06
test(bus): make channel capacity reuse mismatches observable
InternalEventBus::get_or_create_channel returns an existing channel for a name and silently
discards the requested capacity, so the first requester sizes a channel for the whole process.
Several call sites depend on that silence, and five of them survived an earlier fix precisely
because nothing reported it.

Record each distinct (name, actual, requested) disagreement and warn once. The warning is emitted
outside the lock so a sink cannot re-enter the bus while mu_ is held. remove_channel now drops a
name's records too, so the list describes live disagreements and an assertion cannot become
order-dependent. The spdlog include is guarded with __has_include, matching
pressure_limited_poller.h, since this is an installed public header.

The recorded capacity is stored beside the type-erased channel pointer rather than read back through
static_pointer_cast<SpscQueue<T>>. A name is NOT guaranteed to map to one T: "model.events" is
registered as SpscQueue<ModelReadyEvent> at plugins/onnx/model_provider.cpp:1076 and as
SpscQueue<ModelLoadFailedEvent> at :1089/:1266/:1503, so reading capacity() through the cast would be
a wrong-type member access on a reachable production path. The cast itself is still unvalidated, but
that risk is unchanged from what callers already accepted for a doubly-registered name.

On the test side the drain helpers no longer pin a capacity: they drain and remove, so each case's
own declared capacity applies. The two embed_jobs sites that want a small ring drain first, so they
actually receive 2048 instead of silently inheriting a larger registered ring, and a
ScopedChannelRemoval guard drops that ring at scope exit. The throughput case asserts that no
post_ingest disagreement was recorded, guarding the drain-then-remove behaviour its ring depends on.

Evidence: with the pre-fix pinning helper restored, the diagnostic reports "channel 'post_ingest'
already exists with capacity 32 but 64 was requested" and the new assertion fails (10 assertions, 1
failed); with the fix the case passes 10/10 and the binary is 307/307. The 4-batch outcome itself is
load-dependent and did not reproduce on this machine, so the guard's value is that it detects the
precondition deterministically rather than waiting for a loaded runner.

Two defects in an earlier revision of this commit were found by the entry-review workflow (Gemini
3.8 Flash + Claude Opus, adjudicated) before the push, and are fixed here: one embed_jobs site never
established the 2048 its comment claimed, so the comment and this message were false; and the
capacity read went through the cast while claiming a one-T-per-name invariant that is already false
in production.

This does not reduce the mismatch count in this binary (9 -> 10 -> 9 distinct after these fixes, with
the false direction gone). The
remaining records are deliberate test-local rings plus production disagreements that need separate
fixes: embed_jobs (8192 vs 1000) and post_ingest (1000 vs 4096 in RepairService). The diagnostic also
surfaced a family that was not previously known: PostIngestQueue derives symbol_extraction,
entity_extraction, title_extraction and kg_jobs from its per-instance capacity_ via
boundedStageChannelCapacity, while those channels are process-wide singletons, so the
first-constructed queue sizes them for every later queue (32 vs 512 and 32 vs 64 both observed).

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:37:30 -06:00
trvon
2264cb9776
Merge branch 'main' into experimental
Some checks failed
Secret scan / gitleaks (push) Failing after 8s
Mirror source to Forgejo / mirror (push) Has been skipped
Release Candidate / Validate promotion and preview release (push) Has been cancelled
Tests / Static Analysis (push) Has been cancelled
Tests / Warm Conan & ccache (linux-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (linux-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (windows-x86_64) (push) Has been cancelled
Tests / warm (push) Has been cancelled
Tests / Tests (linux-arm64) (push) Has been cancelled
Tests / Tests (macos-arm64) (push) Has been cancelled
Tests / Tests (linux-x86_64-asan) (push) Has been cancelled
Tests / Tests (linux-x86_64-ubsan) (push) Has been cancelled
Tests / Tests (linux-x86_64) (push) Has been cancelled
Tests / Tests (windows-x86_64) (push) Has been cancelled
- Integrate socket shutdown notify fix (#165) from main.
- Reconcile portability allowlist.

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:36:47 -06:00
Trevon
1189161edb
ci(tests): free unused toolchains on the macOS runner (#174)
Some checks failed
Secret scan / gitleaks (push) Failing after 8s
Mirror source to Forgejo / mirror (push) Has been skipped
Release Candidate / Validate promotion and preview release (push) Has been cancelled
Tests / Static Analysis (push) Has been cancelled
Tests / Warm Conan & ccache (linux-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (linux-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (windows-x86_64) (push) Has been cancelled
Tests / warm (push) Has been cancelled
Tests / Tests (linux-arm64) (push) Has been cancelled
Tests / Tests (macos-arm64) (push) Has been cancelled
Tests / Tests (linux-x86_64-asan) (push) Has been cancelled
Tests / Tests (linux-x86_64-ubsan) (push) Has been cancelled
Tests / Tests (linux-x86_64) (push) Has been cancelled
Tests / Tests (windows-x86_64) (push) Has been cancelled
* ci(tests): free unused toolchains on the macOS runner

The macOS ARM64 lane hit "zstd: error 70 ... No space left on device"
followed by a linker failure. Free multi-gigabyte toolchains this build
never uses before anything is built: Android SDK, dotnet, PowerShell and
Mono.

Deliberately keep Xcode/clang, Homebrew, cmake, ccache, zig, boost and the
Rust toolchain: meson.build builds the libSQL subproject only when cargo is
found and silently falls back to plain sqlite3 otherwise, so deleting
~/.cargo would switch this lane database backend without failing anything.

This does not address the nightly release gate, which currently fails on an
unrelated flaky batch-count assertion in daemon_background_processing.

Signed-off-by: trvon <me@trevon.dev>

* ci(tests): clarify SQLite warning-only fallback in macOS free-disk step

Update comment in .github/workflows/tests.yml to accurately describe that
when Cargo is missing, meson.build:555-572 emits warnings before falling
back to standard SQLite3 without failing the build.

Addresses Copilot review comment on PR #174.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
2026-09-22 07:12:28 -06:00
Trevon
27f133f595
test(daemon): stop pinning the shared post_ingest channel capacity (#170)
Some checks failed
Secret scan / gitleaks (push) Failing after 7s
Mirror source to Forgejo / mirror (push) Has been skipped
Release Candidate / Validate promotion and preview release (push) Has been cancelled
Tests / Static Analysis (push) Has been cancelled
Tests / Warm Conan & ccache (linux-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (linux-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (windows-x86_64) (push) Has been cancelled
Tests / warm (push) Has been cancelled
Tests / Tests (linux-arm64) (push) Has been cancelled
Tests / Tests (macos-arm64) (push) Has been cancelled
Tests / Tests (linux-x86_64-asan) (push) Has been cancelled
Tests / Tests (linux-x86_64-ubsan) (push) Has been cancelled
Tests / Tests (linux-x86_64) (push) Has been cancelled
Tests / Tests (windows-x86_64) (push) Has been cancelled
The case "keeps multi-doc batches when extraction concurrency is low"
asserts an exact batch count (extractionBatches == 3) and produced 4 on a
loaded macOS runner (16+16+15+1).

The binding constraint was not the coalescing window: the drain helper
created the shared post_ingest channel with capacity 32 before any queue
was constructed, and get_or_create_channel ignores the requested capacity
on a name hit, so the queue declared capacity 64 but got a 31-slot ring.
With 48 tasks the producer had to block on the channel until the poller
drained, which made batch boundaries a function of runner load.

Drain and then drop the channel so each case capacity applies, and add the
bus remove_channel() that makes that possible. The exact-count assertions
are kept, not relaxed.

Signed-off-by: trvon <me@trevon.dev>
2026-09-20 16:56:22 -06:00
Trevon
cac8c42634
fix(daemon): keep socket shutdown notify inside the lock (#165)
Some checks failed
Build and Push Docker Images / Build Docker Image (linux/amd64) (push) Failing after 12s
Secret scan / gitleaks (push) Failing after 7s
Mirror source to Forgejo / mirror (push) Has been skipped
Release Please / release-please (push) Failing after 7s
Build and Push Docker Images / Build Docker Image (linux/arm64 native) (push) Has been cancelled
Tests / Static Analysis (push) Has been cancelled
Tests / Warm Conan & ccache (linux-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (linux-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (windows-x86_64) (push) Has been cancelled
Build and Push Docker Images / Create Multi-arch Manifest (push) Has been cancelled
Tests / warm (push) Has been cancelled
Tests / Tests (linux-arm64) (push) Has been cancelled
Tests / Tests (macos-arm64) (push) Has been cancelled
Tests / Tests (linux-x86_64-asan) (push) Has been cancelled
Tests / Tests (linux-x86_64-ubsan) (push) Has been cancelled
Tests / Tests (linux-x86_64) (push) Has been cancelled
Tests / Tests (windows-x86_64) (push) Has been cancelled
execute_on_io_context posted a handler that set "done" under the lock
and then called cv.notify_one() after releasing it. The mutex and
condition variable are stack locals of the calling frame, so the waiter
could wake in that window, return, and destroy them -- leaving the IO
thread signalling a destroyed condition variable in reused stack memory.
On shutdown that surfaced as an ASan BUS with a corrupted return address
from SocketServer::stop().

Notify while still holding the lock, so the waiter cannot return before
the notify completes.

Release-As: 0.20.0

Signed-off-by: trvon <me@trevon.dev>
2026-09-19 20:47:59 -06:00
github-actions[bot]
c178f98619
chore: promote experimental to main (#156)
Some checks failed
Build and Push Docker Images / Build Docker Image (linux/amd64) (push) Failing after 58s
Secret scan / gitleaks (push) Failing after 7s
Mirror source to Forgejo / mirror (push) Has been skipped
Release Please / release-please (push) Failing after 14s
Build and Push Docker Images / Build Docker Image (linux/arm64 native) (push) Has been cancelled
Build and Push Docker Images / Create Multi-arch Manifest (push) Has been cancelled
Tests / Static Analysis (push) Has been cancelled
Tests / Warm Conan & ccache (linux-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-arm64) (push) Has been cancelled
Tests / Warm Conan & ccache (linux-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (macos-x86_64) (push) Has been cancelled
Tests / Warm Conan & ccache (windows-x86_64) (push) Has been cancelled
Tests / warm (push) Has been cancelled
Tests / Tests (linux-arm64) (push) Has been cancelled
Tests / Tests (macos-arm64) (push) Has been cancelled
Tests / Tests (linux-x86_64-asan) (push) Has been cancelled
Tests / Tests (linux-x86_64-ubsan) (push) Has been cancelled
Tests / Tests (linux-x86_64) (push) Has been cancelled
Tests / Tests (windows-x86_64) (push) Has been cancelled
* fix(storage): retry reference counter init on transient DB lock

The add-sandbox smoke test (and CLI startup generally) can fail with
'database is locked' when a prior async batch commit holds the reference
database longer than ReferenceDB's short retry budget, which CI runners
hit consistently while local runs pass. Retry the whole init (schema +
migrations) with a bounded, growing backoff (up to ~12s total) so
transient contention no longer fails startup.

Adds a Catch2 test that holds a write lock across init and releases it
mid-retry.

* refactor: deduplicate trim/escapeRegex/hasWildcard helpers

The same string helpers were copy-pasted across CLI and service code:
trim/trimCopy in 5 files, escapeRegex in 3, hasWildcard in 2, and the
regex meta-char table in 2. Move canonical versions to
yams::common::string_utils.h (trimCopy, hasWildcard, escapeRegex,
isRegexMetaChar) and wire all call sites to them; daemon_helpers'
trim_copy now delegates. Add a Catch2 test for the shared helpers.

Kept isTransientMetadataError and annotateResultRelations untouched:
they have semantically different implementations and need a separate
review, not a mechanical merge.

* refactor(cli): share time filters between get/list

Same 6-field time filter logic copy-pasted in GetCommand and ListCommand
(~72 lines each). Extract to yams::cli::applyDocTimeFilters + DocTimeFilters,
wire both commands, add unit test.

* refactor: dedup retry helpers + test fixtures

- isTransientMetadataError + MetadataTelemetry: 2 copies (grep/search)
  -> shared service_utils.hpp
- grep_service local string_contains -> shared features::string_contains
- ArtifactGuard: 5 identical copies in corruption tests -> shared
  tests/common/sqlite_corruption.h

retryMetadataOp loops stay split (async coroutine vs sync sleep - real
difference). annotateResultRelations/applyFileTypeFilters/formatSnippet
deferred: behavior parity question, not mechanical dedup.

* perf(storage): slice-by-8 CRC32 on all crc paths, 13.7x faster

Profiling running daemon: bit-by-bit CRC32 top CPU consumer (2289
samples/15s during grep). Same algorithm/polynomial, byte-identical
output (oracle test 0..512 len), stored checksums stay valid.

Applies slice-by-8 everywhere crc ran:
- yams::common::crc32 (+ crc32Update seeded variant for mid-stream)
- compression::calculateCRC32/updateCRC32 delegate to it
- storage-local duplicate copy removed (dead scope)
- WAL entry checksum copy removed

Also restructures sizeof arithmetic in WAL size math so opengrep does
not misread it as pointer arithmetic.

* fix(daemon): clarify IPC response mismatches

* test: isolate integration state

* refactor(plugins): centralize lifecycle policy

* perf(topology): streamline artifact construction

Reuse admitted relations and dense splitter state while preserving deterministic identities, centroids, and the 64-document safety cap.

Add corpus-shaped benchmarks so construction regressions stay measurable.

* perf(daemon): trust clean metadata restarts

* perf(vector): persist reusable warm starts

* bench(ingestion): validate Simeon lifecycle

* fix(daemon): preserve ingestion admission

* fix(daemon): propagate startup failures

* refactor(config): centralize runtime paths

* refactor(config): unify environment policy

* test: isolate process environment

* refactor(config): unify flat TOML lookups

* fix(mcp): restore WASI final linkage

Keep config and native subsystems out of the minimal archive and export the profile definition so consumer ABI matches.

* refactor(config): consolidate embedding policy

* refactor(bench): isolate benchmark controls

* refactor(metadata): extract content transaction seams

* refactor(test): consolidate daemon harness

* refactor(config): consolidate runtime tuning

* refactor(bench): type run configuration

* perf(search): optimize concept mining warmup

* test(policy): gate configuration authority

* fix(config): stack daemon environment leases

* fix(config): revoke removed tuning overrides

* fix(bench): reject unknown arguments

* fix(build): restore clean test and benchmark links

* build(deps): update Simeon concept mining

Point the submodule at the reviewed, published optimization so clean CI clones can resolve it.

* fix(ci): stabilize sanitizer validation

Rebuild macOS TSan outputs so compiler instrumentation and compiler-rt come from one Apple toolchain generation. Preserve daemon fixture dependencies until ServiceManager teardown so sanitizer lanes do not exercise a test-only use-after-free.

* style(tests): align validation formatting

* fix(plugins): honor lifecycle RPC timeout

Use ExternalPluginHostConfig::defaultRpcTimeout for discovery, load, initialization, health, restart, and shutdown calls so sanitizer-slow plugin startup does not fall back to JsonRpcClient's hidden three-second default.

* fix(config): distinguish empty from unset env on Windows

_putenv_s removes a variable for "" and returns EINVAL for NULL, so the
CRT environment cannot represent an explicitly empty value; the boundary
collapsed unset and empty into one state. Read/write through the Win32
environment (SetEnvironmentVariableA/GetEnvironmentVariableA) instead,
keeping a best-effort _putenv_s sync so legacy std::getenv readers stay
coherent, and treat the empty-vs-unset read race fail-closed.

* test(metadata): clone DB template via VACUUM INTO

copy_file of the migrated WAL-mode template plus -wal/-shm sidecars
corrupts WAL replay on Windows (stale -shm), losing kg_nodes. VACUUM
INTO produces a self-contained single-file copy; a probe verifies the
KG schema survived the clone.

* test(portability): add cross-platform lint gate

Catch Windows/Linux/macOS-only defect classes before push: raw
std::jthread, sqlite3_open* via bare path.c_str(), and raw getenv
reads outside the config boundary. Wired into pre-commit and the
unit/policy suite; 575 reviewed sites allowlisted.

* ci(cross): add compile-only cross-compile smoke

Detects mingw-w64/linux-gnu cross compilers and compiles a portability
probe with -fsyntax-only to catch narrow-vs-wide API and std::jthread
issues before GitHub CI. Skips cleanly when no toolchain is present;
--self-test proves dispatch and failure propagation with a fake compiler.

* ci(gate): distinguish transient infra from real failures

Retry setup.sh dependency resolution up to 3 times on Conan network
timeouts with backoff; report a distinct infra-retry (exit 77) when the
network failure persists, while real compile/test failures still fail
hard. Prevents a transient center2.conan.io timeout masquerading as a
code failure.

* test(storage): tag refcount stress cases slow

Stress cases with 20 threads / 200 iterations run in the default unit
lane because only ~[slow] is filtered. Tag them [slow] so they move to
the storage_submodule_slow suite.

* test: move stress cases to slow suite

Stress-tagged cases ran in the default unit lane because only ~[slow]
was filtered. Tag them [slow] and add ~[slow]/[slow] splits to the five
binaries (mcp_submodule, work_coordinator, daemon_background,
plugin_infrastructure, hybrid_search) so they run in the slow suite.

* feat(memory-sync): add version-vector + LWW data model

Version vector (element-wise max merge, dominates/concurrent partial
order) plus hybrid timestamp and LWW resolution: causally-later writes
win, concurrent writes resolve deterministically by (ts, origin). JSON
serialization via nlohmann. Header-only, no library deps.

* feat(memory-sync): add backend-agnostic sync loop

Publish memory records as content-addressed blobs plus a
version-vector-tagged index record per write; sync lists the index,
pulls records, and LWW-merges to a deterministic winner per logical
key. Backend injected (filesystem / S3-R2 interchangeable).

* feat(memory-sync): add periodic sync service

Daemon-facing service owns the backend + sync loop and reconciles on a
configurable interval via a stoppable worker thread. Typed
MemorySyncConfig (nodeId, syncIntervalMs); start/stop idempotent and
stop noexcept-safe. Backend injected so filesystem and S3/R2 resolution
stay with the caller.

* refactor(cli): extract already-initialized init path

Factor the alreadyInitialized && !force_ branch out of the monolithic
execute() into a private handleAlreadyInitialized() helper. Behavior
preserved; setup decision/try nesting reduced. Portability allowlist
line numbers updated for the shift.

* refactor(cli): extract testable post-init guidance

Factor the post-setup 'what next / how to configure' guidance into pure
formatLaterCommand/formatInitSummary builders so each decision branch's
help is testable without stdout, and add a focused test asserting the
later-command and ready-summary text. Output is byte-identical.
Portability allowlist line numbers updated for the shift.

* feat(memory-sync): add daemon config + service factory

Typed MemorySyncDaemonConfig parsed from the [memory_sync] TOML section
(enabled, node_id, backend, path, sync_interval_ms) plus
createMemorySyncService() which resolves filesystem/s3 backends and
constructs the service. No YAMS_* env knobs.

* feat(memory-sync): wire service into daemon config + lifecycle

Add a typed [memory_sync] TOML section (enabled, node_id, backend,
path, sync_interval_ms) parsed into DaemonConfig, and start/stop the
MemorySyncService within ServiceManager: start after content-store init
(best-effort, relative paths resolve against the data dir), stop noexcept
in Phase 8 shutdown. No YAMS_* env knobs.

* feat(cli): add yams p2p publish/read/status

Thin p2p command over the memory-sync service: publish <key> <value|@file>,
read <key>, and status (merged index summary + backend identity). Binary
values are hex-encoded in JSON output; config resolves from [memory_sync]
with --backend/--path/--node-id overrides.

* test(p2p): add container convergence harness

docker-compose with two saged daemons + MinIO (R2 stand-in) + shared
volume, a multi-stage saged.Dockerfile, and a validation script driving
filesystem<->filesystem convergence and causal-ordering determinism
(scenarios 1 + 3).

* fix(memory-sync): serialize loop access + resume causal history

Add a mutex around all MemorySyncLoop access so the periodic sync worker
does not race publish/read/syncOnce on the loop's mutable version-vector
and logical-clock state. Reconcile before publish so a fresh loop instance
(e.g. a stateless CLI) resumes its node's causal history. Reject a zero
sync interval.

* fix(memory-sync): parse s3 URL via storage resolver

Route the s3 backend through StorageBackendFactory::parseURL so scheme
detection, authority credentials, and query params flow through the same
path as the daemon's storage resolver, and reject a backend=s3 config
whose path is not an s3:// URL.

* test(daemon): make lock-holder test fork-safe

The child forked by 'Daemon does not terminate unverified data-dir lock
holder' built an fs::path and called json::dump(), both of which allocate;
a fork in a multithreaded process can leave the malloc lock held, so the
child could fail before signaling readiness. Pre-compute the lock path in
the parent, format the payload with snprintf into a stack buffer, signal a
failure byte on open/flock error, and retry the parent's read on EINTR.
Portability allowlist line numbers updated for the shift.

* feat(storage): add sync backend primitives

* feat(memory-sync): authenticate records

* fix(memory-sync): bound reconciliation

* feat(memory-sync): manage session lifecycle

* feat(memory-sync): add metadata adapters

* feat(memory-sync): add vector adapter

* feat(daemon): wire bounded sync lifecycle

* feat(p2p): expose sync protocol status

* fix(vector): bind rebuild readiness to epochs

* docs(p2p): define local validation contract

* fix(daemon): harden async shutdown

* fix(metadata): normalize extraction status

* test(portability): reject analyzer shims

* test(ci): stabilize platform matrix

* test(ci): fix remaining platform builds

* ci(sourcehut): stream runtime artifact

* fix(config): preserve empty Windows values

* fix(memory-sync): escape acknowledgement keys

* test(ci): normalize Windows expectations

* fix(config): track empty Windows values

* test(daemon): synchronize repair hysteresis setup

* chore(tooling): mark dependency-managed includes

* style(ci): format artifact collector

* ci(github): bound apt mirror retries

* test(ci): isolate ARM smoke lifecycles

* fix(daemon): serialize socket accept shutdown

* fix(daemon): preserve stamp claim ownership

* test(memory-sync): scale convergence waits

* fix(daemon): bypass strand close during global teardown

* fix(storage): sign S3 wire path verbatim

* fix(cli): discover daemon log via status

* feat(memory-sync): log quarantine reasons and config summary

* fix(cli): render disabled vectors and quiet data-dir alarm

* chore(storage): annotate reviewed env reads

* feat(cli): install daemon as systemd service

* fix(memory-sync): include spdlog in public header

* fix(memory-sync): log quarantine from daemon, not header

* fix(cli): discover live daemon socket for p2p

* test(extraction): poll for launcher response lines

* fix(daemon): complete pool futures on cancellation

* test(daemon): fix pool teardown race and scale hysteresis

* fix(daemon): keep p2p status off sync lock

* test(daemon): harden background wait, slow-lane hysteresis

* chore(topology): remove parked join knob (#62)

Signed-off-by: trvon <git@trevon.dev>

* fix(graph): scope health checks to graph data (#63)

Signed-off-by: trvon <git@trevon.dev>

* fix(retrieval): harden one-shot output (#64)

Deduplicate unary results, verify direct blobs before writing, and surface stream finalization failures instead of reporting partial output as successful.

Signed-off-by: trvon <git@trevon.dev>

* perf(search): favor lexical mixed defaults (#65)

Signed-off-by: trvon <git@trevon.dev>

* perf(vector): harden portable backends (#66)

* perf(vector): update portable backends

Keep generic release binaries on baseline ISA until runtime CPU dispatch can guard AVX and FMA paths.

Signed-off-by: trvon <git@trevon.dev>

* test(vector): isolate backend measurements

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <git@trevon.dev>

* feat(storage): expose typed disk pressure policy

Signed-off-by: trvon <git@trevon.dev>

* fix(mcp): preserve embedded stdio buffers

Move process-global stream setup to the MCP executable so embedded transports retain caller-owned redirection. Add a regression for the failing construction order.

* feat(p2p): add authenticated direct replication (#68)

* fix(memory-sync): keep replication responsive

Skip poisoned blobs without wedging backfill and publish immutable status snapshots so daemon IPC does not block on an active reconciliation cycle.

Reconstructed from ff62d3f8, 8e232721, and e60f513d.

Signed-off-by: trvon <git@trevon.dev>

* feat(p2p): add authenticated direct transport

Require TLS 1.3 mutual identity and bind the authenticated certificate, node, corpus, and epoch before replication state is exchanged.

Reconstructed from 96f9db88 and c46de77b.

Signed-off-by: trvon <git@trevon.dev>

* feat(p2p): exchange causal deltas

Persist peer pins and causal resume state before visibility so reconnects continue bounded authenticated replication without becoming remote query proxies.

Reconstructed from 1b8be146 and 41883338.

Signed-off-by: trvon <git@trevon.dev>

* feat(p2p): integrate direct sync services

Wire listener, reconnect, IPC, CLI, peer persistence, and daemon lifecycle around the authenticated direct transport.

Reconstructed from a19aaa44.

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): apply replicated document state

Index authenticated remote winners, propagate document tombstones, and keep service/test wiring complete across daemon paths.

Reconstructed from 4ca660c6, 443253f1, 3e9db24d, and 4b2ddcc5.

Signed-off-by: trvon <git@trevon.dev>

* test(p2p): register direct sync gates

Replace the superseded internal contract with operator-facing direct-sync guidance and register daemon integration coverage in the standard test layout.

Reconstructed from 360b9115 and 89416ccb.

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): require bounded peer enrollment

Bound aggregate sessions and worker lifetimes, create owner-only identity keys atomically, and reject unknown inbound peers until operators pin their mTLS identity.

Reconstructed from f91be528, 79de8e48, 4fb1e4b8, dc30a6c0, and b329edd3.

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): persist quarantine and deletes

Persist full-history writer commitments before invalidating forked state, and order local deletion through a durable authenticated outbox so tombstones survive crashes and reconnects.

Reconstructed from 74ce01e0 and 19888604.

Signed-off-by: trvon <git@trevon.dev>

* feat(p2p): add authenticated cold bootstrap

Install bounded zero-state snapshots only from previously operator-pinned peers, bind them to exact frontiers and full-history commitments, and recover staged journals before replication can resume.

Reconstructed from 8a9f5acf.

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): replicate content deletion (#69)

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): resume bounded history catch-up (#70)

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): harden transport availability (#71)

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): harden bootstrap trust (#72)

Require authenticated direct stores and explicit operator enrollment while hardening private keys and trust files across Unix and Windows. Sign local control artifacts, bind them to a protected store generation, and serialize async initialization shutdown.

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): serialize sync application (#73)

Signed-off-by: trvon <git@trevon.dev>

* test(p2p): add direct control fuzz harnesses (#74)

Add libFuzzer/AFL++ harnesses for the direct-P2P handshake/state/history and
delta/bootstrap control parsers. Fuzzers invoke the same production parser
seams as the live wire path, so coverage cannot drift from real validation. A
protocol-v3 seed exercises exact-version rejection after the v4 upgrade. Bound
noninteractive AFL++ campaigns with AFL_FUZZ_SECONDS and register focused
fuzz-smoke tests without duplicating broad protocol/delta registrations.
Clarify that peer/cycle status is local health telemetry rather than global
convergence, and document the remaining decision-grade live-performance gate.

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): harden direct-P2P framing and ingress admission (#75)

Bound pre-authentication JSON nesting depth to 32 so a pathologically nested
control frame cannot overflow the session-thread stack before any trust
decision. Raise the control-frame cap to 512 KiB to cover the worst-case
authenticated state frame (~300 KiB across 256 writers x 256-byte ids). Gate
MemorySyncLoop::applyDeltas through the resource governor so a peer cannot
flood deltas on the direct replication path. Account delta/bootstrap wire bytes
using the exact received frame size instead of a compact re-serialization.

Signed-off-by: trvon <git@trevon.dev>

* fix(p2p): harden CLI status and operator/key-rotation flows (#76)

Keep `p2p status` text output machine-parseable by removing ANSI severity
colorization, and stop connect/disconnect/forget from throwing on malformed
daemon responses (safe .value defaults, uint64 counters). Bound handshake and
delta timeouts to kP2pMaxOperationTimeout up front, and ceiling direct
sync_interval_ms to the 5-minute reconnect bound so invalid config fails early
instead of aborting daemon startup. Degrade memory-sync status to peerCount=0
on a transient peer-registry read instead of failing the whole status IPC. Add
an explicit `p2p forget` operation (proto FORGET=9 through dispatcher, manager,
and registry) so operators can rotate a peer's pinned key while preserving
fail-closed enrollment.

Signed-off-by: trvon <git@trevon.dev>

* refactor(p2p): tighten tests, share helpers, and guard test hooks (#77)

Compile the direct-P2P test-only machinery out of production builds
(YAMS_DAEMON_TEST_HOOKS_ENABLED now wraps the reconnect-loop hook storage,
invoker, and call site, matching the already-guarded setter). Replace the
single shared writer key in p2p_manager tests with one distinct key pair per
writer identity so origin-binding regressions (a record claiming origin X but
signed by Y's key) fail verification. Extract the copy-pasted bytes/text/TempDir
helpers into tests/common/p2p_test_helpers.h and reuse them across the
transport/delta/manager suites. Replace a timing-sensitive fixed sleep in the
transport session-expiry test with a bounded poll that accepts both deadline
manifestations, and gate the memory-sync-service concurrent-lifecycle stress
case behind YAMS_RUN_STRESS_TESTS=1 per the existing FTS5 pattern. Deduplicate
the byte-identical commitment-JSON serialization inside MemorySyncLoop
(coldBootstrapRoot / persistReplicationCheckpoint) into one static helper.

Signed-off-by: trvon <git@trevon.dev>

* refactor(daemon): decompose ServiceManager responsibilities (#79)

* fix(ci): enable OpenGrep custom ignore support

Signed-off-by: trvon <me@trevon.dev>

* fix(p2p): centralize backoff cap and bound test polling

Signed-off-by: trvon <me@trevon.dev>

* refactor(daemon): split ServiceManager responsibilities

Signed-off-by: trvon <me@trevon.dev>

* fix(daemon): narrow ServiceManager test-hook declarations

Signed-off-by: trvon <me@trevon.dev>

* chore(architecture): ignore local Brick artifacts

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* refactor(daemon): apply Brick audit fixes to the PR #79 decomposition (#80)

* feat(storage): expose typed disk pressure policy

Signed-off-by: trvon <git@trevon.dev>

* fix(mcp): preserve embedded stdio buffers

Move process-global stream setup to the MCP executable so embedded transports retain caller-owned redirection. Add a regression for the failing construction order.

* refactor(daemon): scope shared P2P protected-file accessors for cross-TU use

The protected-file accessors (readProtectedP2pPrivateKey,
readProtectedP2pTrustFile, writeExclusiveP2pPrivateKey) are shared by the
P2P identity-key paths and the memory-sync bootstrap. Keep their
platform-specific definitions in p2p_integration.cpp unchanged and scope
them into yams::daemon::service_manager_detail, declared by the private,
non-installed header p2p_protected_file.h. Qualify the internal
callsites. No behavior change.

Prepares moving ServiceManager::initializeMemorySync into the
memory-sync translation unit (Brick audit cohesion note).

Signed-off-by: trvon <me@trevon.dev>

* refactor(daemon): place initializeMemorySync with memory-sync TU

Brick structural audit of PR #79 flagged that
ServiceManager::initializeMemorySync lives in p2p_integration.cpp while
the rest of the memory-sync surface lives in memory_sync.cpp. Move it —
together with its exclusive helper ensureAuthenticatedDirectStore — into
memory_sync.cpp.

The shared protected-file accessors remain defined in
p2p_integration.cpp (see the preceding service_manager_detail commit);
moved function bodies are unchanged and ServiceManager.h is untouched.

Brick evidence (experimental pre-79 af609a1e vs post-79 4d942793):
ServiceManager.cpp functions 84 -> 59 with 25 moved and 0 vanished;
ServiceManager.cpp outgoing call/import edges 1292 -> 954, distinct
targets 59 -> 41.

Validation: full ninja build green; meson test yams:service_manager,
yams:service_manager_shutdown, yams:service_manager_degraded — 3/3 OK.

Signed-off-by: trvon <me@trevon.dev>

* refactor(vector): break simeon_embedding_backend.h include cycle with embedding_generator.h

simeon_embedding_backend.h only declares functions taking EmbeddingConfig
by reference, yet included the full embedding_generator.h definition,
forcing every consumer of the simeon backend header to transitively pull
the generator header (and feeding the embedding_generator <->
simeon_embedding_backend 2-cycle reported by the Brick structural survey).

Replace the include with forward declarations for IEmbeddingBackend and
EmbeddingConfig. Consumers that use the full definitions
(simeon_embedding_backend.cpp, search_engine.cpp,
ingestion_throughput_bench.cpp) now include embedding_generator.h
directly.

Brick caveat: the tool's lexical lane cannot see the header-include edge
removal (0.65-confidence interface-dispatch misattribution keeps a
file-level cycle in its graph); the include-cycle break is verified at
source level. Brick survey pre/post artifacts kept in
worktree build/brick/architecture-iter{,-after}.json.

Validation: full ninja build green; meson test yams:vector_submodule,
yams:simeon_model_provider, yams:embedding_lifecycle,
yams:embedding_recovery, yams:service_manager — 5/5 OK.

Signed-off-by: trvon <me@trevon.dev>

* refactor(vector): extract computeCosineSimilarity from VectorDatabase into vector_utils

The pure cosine-similarity helper was stranded as a static member of the
VectorDatabase storage class, forcing sqlite_vec_backend.cpp to include
vector_database.h for a pure math function (Brick deferred lead #2 from
the PR #79 audit loop).

Move the implementation to vector_utils as a free function
(yams::vector::vector_utils::computeCosineSimilarity, semantics copied
verbatim: 0.0 on size mismatch or zero norm), keep
VectorDatabase::computeCosineSimilarity as a forwarding static for the
external caller in doctor dedupe, and switch the four backend callsites
to the free function. sqlite_vec_backend.cpp no longer includes
vector_database.h.

Validation: full ninja build green; meson test vector_submodule (incl.
simeon_backend_catch2_test) and yams:service_manager — green.

Signed-off-by: trvon <me@trevon.dev>

* refactor(daemon): extract client transport types to break sandbox probe header cycle

ClientTransportMode and ClientConfig moved verbatim from daemon_client.h
into a new shared header client_config.h (Brick deferred lead #3 from the
PR #79 audit loop: daemon_client <-> sandbox_detection type-level cycle).

sandbox_detection.h now depends only on client_config.h; its
implementation adds the explicit daemon_client.h include it needs for
the DaemonClient discovery statics. All existing consumers are
unaffected: daemon_client.h includes client_config.h so ClientConfig
remains visible transitively.

Remaining runtime dependency (sandbox_detection.cpp calling
DaemonClient::resolveSocketPathConfigFirst / isDaemonRunning statics,
which forward to socket_utils and a file-local ping helper) is recorded
as the follow-up for full decoupling; the header-level cycle is broken.

Validation: full ninja build green; meson test yams:service_manager,
yams:multiplex_client, yams:jsonrpc_client, vector_submodule — all green.

Signed-off-by: trvon <me@trevon.dev>

* test: refresh portability allowlist line anchors after include shifts

The Brick-audit include additions (search_engine.cpp,
ingestion_throughput_bench.cpp) and removal (sqlite_vec_backend.cpp)
shifted line numbers, making seven portability allowlist entries stale
(same reviewed raw getenv/path usages, new line numbers). Refresh the
anchors; no code or policy change.

Signed-off-by: trvon <me@trevon.dev>

* fix(daemon): include <fstream> for all POSIX /proc probe paths

The /proc readers compile in the non-Windows/non-Apple branch (BSD and
other POSIX targets), not just under __linux__; guard the <fstream>
include with the same condition so those targets compile.

Copilot review finding on PR #80 (src/daemon/components/DaemonMetrics.cpp:7).

Signed-off-by: trvon <me@trevon.dev>

* refactor(daemon): apply storage.disk_pressure outside applyMemorySync

ConfigResolver::applyMemorySync also applied [storage.disk_pressure],
coupling unrelated config parsing to the memory_sync apply path and
producing a misleading refusal message for invalid disk-pressure
policies. Move the call to its natural site in daemon_main before the
memory_sync apply, with its own accurate refusal message. Same startup
semantics (exit 1 on invalid policy), correct attribution.

Copilot review finding on PR #80 (src/daemon/components/ConfigResolver.cpp:1658).

Signed-off-by: trvon <me@trevon.dev>

* test(daemon): update applyMemorySync no-op expectation after disk-pressure decoupling

applyMemorySync no longer applies [storage.disk_pressure] (moved to its
own call site in daemon_main), so a section set without memory_sync
entries is now a no-op rather than a disk-pressure rejection. The
invalid-disk-pressure rejection is still asserted directly against
applyStorageDiskPressure in the preceding checks.

Caught by the pre-push CI gate (asan+tsan: daemon_component_submodule).

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <git@trevon.dev>
Signed-off-by: trvon <me@trevon.dev>

* feat(metrics): surface WAL data-system optics in daemon status

WalMetricsProvider::getStats() was wired (attachWalManager) but never
consumed anywhere: WAL active/pending/total entries, bytes, and log-file
count were invisible to operators. Close the optics gap:

- MetricsSnapshot: add walActiveTransactions/PendingEntries/TotalEntries/
  TotalBytes/LogFileCount (detailed snapshot only).
- DaemonMetrics::enrichDetailedSnapshot: populate from
  ServiceManager::getWalMetricsProvider() (cheap atomic reads).
- request_dispatcher_status: emit wal_* requestCounts on detailed status.
- metric_keys.h: kWAL* keys (wal_active_transactions, ...).
- status_command --json: render a 'wal' object when counters are nonzero.
- tests: WalMetricsProvider pass-through unit tests (zeros without a
  manager; forwards WALManager stats; reset on null manager).

Validated: build/debug suite (ConfigResolver 42 cases, metrics core/status,
new WAL provider tests) plus brick (0 new cycles on touched files) and fold
(0 findings on added lines).

* chore: remove dead functions and address Fold triage leaks (#82)

* chore: remove dead post-ingest helper

Remove shouldPrepareSequentially, the sole Fold unused-function finding
confirmed dead by clean ASan and TSan builds. All other 45 findings have
live callers or uses that Fold's bounded model missed and remain intact.

Fold provenance: build/fold/release-prep-79.sarif (2026-09-01 run).

Signed-off-by: trvon <me@trevon.dev>

* chore(plugins): document Fold ownership

Document 6 proved memory-leak findings as intentional ownership:
process-lifetime Ort::Env singletons and host-freed C-ABI result data.
Refresh portability allowlist anchors shifted by the comments.

Fold provenance: build/fold/release-prep-79.sarif (2026-09-01 run).

Signed-off-by: trvon <me@trevon.dev>

* fix(storage): treat reserve boundary as emergency

Classify available bytes equal to the configured emergency reserve as Emergency, matching the hard-minimum contract and operator message that the reserve has been reached. Add a regression assertion at the exact boundary.

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>

* refactor(vector): decouple embedding generator policy (#83)

* refactor(vector): add injectable executor for the embedding daemon bridge

await_with_timeout spawned coroutines unconditionally on the daemon
global IO context, coupling the vector-layer embedding generator to
daemon globals. EmbeddingConfig gains an optional
boost::asio::any_io_executor executorOverride; the bridge uses it when
present and keeps the global executor as the default — identical
behavior for all existing callers (embedding_service, model providers,
CLI all construct without an override).

Lead 1 plan step (b) of the embedding_generator vector->daemon
decomposition (Brick audit follow-up).

Validation: full ninja build green; meson test yams:service_manager,
vector_submodule, yams:embedding_lifecycle, yams:embedding_recovery —
all green.

Signed-off-by: trvon <me@trevon.dev>

* refactor(vector): split DaemonBackend into own TU

Keep daemon client/IPC headers out of the vector-layer generator TU;
EmbeddingGenerator builds the backend via createDaemonBackend factory
on the existing IEmbeddingBackend seam. Behavior-preserving.

Brick audit lead 1: 5 file-level cycles hubbed on
embedding_generator.cpp.

Signed-off-by: trvon <me@trevon.dev>

* revert(vector): keep bridge executor out of public EmbeddingConfig

Pre-push gate caught it: the config layer includes the vector header
transitively (ConfigResolver.h -> document_chunker.h ->
embedding_generator.h) and has no boost asio include path, so
<boost/asio/any_io_executor.hpp> in the public header breaks the
config build.

Executor selection stays in embedding_daemon_backend.cpp (daemon-facing
TU by design); per-instance injection needs the config->vector include
direction fixed first — recorded as follow-up.

Signed-off-by: trvon <me@trevon.dev>

* refactor(vector): trust resolved backend policy

Remove EmbeddingGenerator's ambient ConfigResolver read. Callers now own
backend-policy resolution; EmbeddingService marks its already-resolved
runtime policy unconditionally. Keep TuneAdvisor only for the process-wide
concurrency gate, where instance-local policy would not coordinate admission.

Tests pin typed backend authority against conflicting environment state.

Signed-off-by: trvon <me@trevon.dev>

* chore(vector): remove stale config include

The generator no longer reads config helpers after backend policy moved to
callers, so drop the misleading DaemonBackend comment and unused include.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* docs: replace SourceHut with Forgejo mirror (#91)

Signed-off-by: trvon <me@trevon.dev>
Co-authored-by: trvon <me@trevon.dev>

* refactor(daemon): narrow checkpoint WAL capability (#84)

* refactor(daemon): decouple checkpoint repository

Signed-off-by: trvon <me@trevon.dev>

* fix(daemon): preserve partial checkpoint fallback

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>

* refactor(repair): share embedding candidate policy (#85)

* refactor(repair): unify embedding candidate policy

Signed-off-by: trvon <me@trevon.dev>

* test(repair): exercise candidate projection seam

Override the concrete repository grep projection in the test double so the
candidate-policy characterization exercises its in-memory documents.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* refactor(search): extract auxiliary vector merge (#86)

* refactor(search): extract auxiliary vector merge

Signed-off-by: trvon <me@trevon.dev>

* fix(search): preserve auxiliary tie behavior

Keep the extracted merge comparator identical to the original score-only sort
and avoid prescribing equal-score order in characterization tests. Qualify the
search detail namespace so the topology test compiles unambiguously.

Signed-off-by: trvon <me@trevon.dev>

* test(search): avoid auxiliary score tie

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>

* refactor(daemon): extract bootstrap status builder (#87)

Signed-off-by: trvon <me@trevon.dev>

* refactor(daemon): extract post-ingest NL graph builder (#88)

* refactor(daemon): extract post-ingest NL graph builder

Signed-off-by: trvon <me@trevon.dev>

* fix(daemon): preserve post-ingest graph behavior

Keep the extracted graph builder private, preserve the original confidence-only
pointer ordering before copying concepts, avoid defining tie order, and use a
C++20-safe identifier. Make path expectations follow production normalization.

Signed-off-by: trvon <me@trevon.dev>

* fix(daemon): include move utility directly

Signed-off-by: trvon <git@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>

* ci: mirror approved refs to Forgejo

Signed-off-by: trvon <me@trevon.dev>

* ci: pin experimental release source

Signed-off-by: trvon <me@trevon.dev>

* ci: attest experimental release artifacts

Signed-off-by: trvon <me@trevon.dev>

* ci: isolate experimental package repositories

Signed-off-by: trvon <me@trevon.dev>

* ci: follow repository publisher ownership

Signed-off-by: trvon <me@trevon.dev>

* ci: make immutable uploads atomic

Signed-off-by: trvon <me@trevon.dev>

* ci: check S3 content type invariant

Signed-off-by: trvon <me@trevon.dev>

* ci: disable Forgejo host verification

Signed-off-by: trvon <me@trevon.dev>

* ci: use direct Forgejo SSH host

Signed-off-by: trvon <me@trevon.dev>

* fix(metadata): keep replica derivation readiness local

Signed-off-by: trvon <me@trevon.dev>

* fix(retrieval): preserve content identities in JSON results

Signed-off-by: trvon <me@trevon.dev>

* fix(daemon): deliver immediate in-process responses once

Signed-off-by: trvon <me@trevon.dev>

* fix(cli): preserve content bytes during hash hydration

Signed-off-by: trvon <me@trevon.dev>

* fix(retrieval): propagate metadata lookup failures

Signed-off-by: trvon <me@trevon.dev>

* fix(memory-sync): require explicit shared corpus scope

Signed-off-by: trvon <me@trevon.dev>

* feat(memory-sync): validate traceable record-only task revisions

Signed-off-by: trvon <me@trevon.dev>

* fix(daemon): retain async initialization completion ownership

Signed-off-by: trvon <me@trevon.dev>

* fix(metadata): cancel integrity validation without quarantine

Signed-off-by: trvon <me@trevon.dev>

* fix(ingest): preserve extractor metadata through enrichment

Signed-off-by: trvon <me@trevon.dev>

* fix(embedding): avoid deferred shutdown access before start

Signed-off-by: trvon <me@trevon.dev>

* fix(embedding): reject stale prepared text snapshots

Signed-off-by: trvon <me@trevon.dev>

* fix(metadata): invalidate embeddings when extracted input changes

Signed-off-by: trvon <me@trevon.dev>

* feat(metadata): add local embedding derivation tokens

Signed-off-by: trvon <me@trevon.dev>

* perf(search): reuse Simeon scores within a request

Signed-off-by: trvon <me@trevon.dev>

* perf(search): refill chunk-crowded vector results

Signed-off-by: trvon <me@trevon.dev>

* formal(topology): distinguish allowed-set retrieval

Signed-off-by: trvon <me@trevon.dev>

* fix(topology): bind calibration to routing policy identity

Signed-off-by: trvon <me@trevon.dev>

* ci: fetch complete Forgejo mirror history (#101)

* ci: fetch full mirror history

Signed-off-by: trvon <me@trevon.dev>

* Fix git remote add command syntax

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: trvon <me@trevon.dev>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(ingest): read shared post-ingest content in place

dispatchNonEmbeddingStages hands one shared_ptr<vector<byte>> to the KG,
symbol, and entity channels. The entity stage moved out of that buffer, so
whichever consumer ran after it saw zero bytes and silently extracted
nothing. The KG batch consumer already read through a pointer; the entity
stage now does the same, keeping an owned fallback only for the
store-reload path.

Also delete processSymbolExtractionStage, the legacy single-item symbol
handler: it had no callers, and it moved out of the shared buffer as well
before copying the bytes a second time into contentUtf8.

Adds a YAMS_TESTING hook and a Catch2 case that runs two entity jobs over
one shared buffer and asserts the buffer survives.

* fix(topology): rebuild stale memberships (#114)

* fix(topology): rebuild stale memberships

Incremental reuse can retain deleted members outside the dirty region.
Rebuild coherent artifacts from current metadata when prior members vanish;
retain final validation, pending dirty work, and unready-corpus deferral.

Cover deleted seeds, partial clusters, empty corpora, dry runs, and late
deletion without changing the live corpus or claiming a speedup.

Signed-off-by: trvon <me@trevon.dev>

* Add cstdint and optional includes to test file

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: trvon <me@trevon.dev>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(wal): require an explicit WAL directory and prune empty segments

WALManager::Config defaulted walDirectory to "./wal", so any manager built
from a default config wrote segments into the process working directory.
The repo root had accumulated 43,577 such files (42,499 zero-byte, 1,078
zero-filled 1 MiB mmap preallocations from killed writers; 1.1 GB, no
entries). The daemon already passes <dataDir>/wal explicitly.

- walDirectory has no default; initialize() returns InvalidArgument when
  it is empty. The default constructor is removed (it had no callers).
- initialize() still derives the sequence from every segment name, then
  deletes plain segments that never received an entry: zero bytes, or a
  zero magic word at offset 0 (a preallocation left by an unclean exit).
  Compressed segments and anything with real bytes are untouched.

Tests: unconfigured directory is rejected; 50 empty + 1 preallocated
segments are pruned, a non-empty one survives, and the sequence still
advances past the pruned names.

Signed-off-by: trvon <me@trevon.dev>

* fix(wal): preserve damaged zero-prefix segments

Only prune a nonempty segment after every byte is proven zero. A zero header alone is not evidence that recovery data is absent. Scan with bounded memory and preserve the file on read failure.

Signed-off-by: trvon <me@trevon.dev>

* chore(plugins): delete the hound experiment and the headers it kept alive (#142)

plugins/hound was a single README for an abandoned experiment, and
tools/hound_import.py imported a yams_sdk.hound module that no longer
exists anywhere in the tree. Removing them also removes the only
mention of createGraphAdapterV1, which let three headers go:

- include/yams/daemon/resource/graph_adapter.h: declared IGraphAdapter
  and three factory functions with no definition and no includer.
- include/yams/daemon/resource/abi_content_extractor_adapter.h: a
  self-described PBI-096 compatibility forwarder with no includer.
- include/yams/core/fsm_helpers.h: shared_ptr wrappers around
  daemon/ipc/fsm_helpers.h with no includer; the one consumer already
  includes the real header.

Proof of zero references (build dirs excluded):
  rg -n 'graph_adapter\.h|abi_content_extractor_adapter|core/fsm_helpers\.h|hound' \
     --glob '!build/**' --glob '!third_party/**'
  → only the deleted files themselves.

Signed-off-by: trvon <me@trevon.dev>

* [03/14] Fence stale embedding readiness using derivation tokens (#143)

* feat(embedding): complete embeddings by derivation token

cbc4959c added derivation tokens (begin/complete with a per-attempt
generation, invalidated by document_content triggers) but nothing
produced them: beginDocumentEmbeddingDerivation had no callers and the
completion op's derivations field was never written, while every ingest
still paid for the triggers and the per-hash NOT EXISTS guard.

processEmbedJob now mints one token per document it will embed, before
any input snapshot is read, and completes by token instead of by hash.
A document whose content changes while it is being embedded keeps
has_embedding = 0 and is re-derived later, instead of publishing vectors
for text the corpus no longer holds. Repair jobs travel the same
channel, so they get the same guarantee. The recipe binds the
preparation policy, embedding space, provider version, and dimension
through the embeddingDerivationRecipe helper that cbc4959c also left
unused.

Repository: batchBeginDocumentEmbeddingDerivations and
batchCompleteDocumentEmbeddingDerivations do a whole job in one
transaction each (the single-token completion loop in WriteCoordinator
opened one transaction per document). WriteCoordinator uses the batch
form and takes its stats lock once.

A small EmbeddingDerivationLedger maps job hashes to their tokens so
completion is issued for exactly the documents that produced vectors.

Behavior note: a re-embed (skipExisting = false) of an already-embedded
document resets its readiness at mint time; if that job then fails, the
document reads as not embedded until the next attempt. That is the
attempt semantics the token table was built for.

Not included: the legacy hash completion path is now unproduced and is
removed in a follow-up so the coalescer change is reviewed on its own.
tests/scripts/portability_allowlist.txt: 13 EmbeddingService.cpp
line anchors reflowed; no new environment reads.

The startup reconcile (ServiceManager, "Reconciling document_embeddings_status
from vector rows") used to mark every document with vector rows as embedded,
which would have undone the stale-token guarantee on the next daemon start: a
document whose content changed under an in-flight embed has vector rows for the
old text. reconcileDocumentEmbeddingStatusByHashes now leaves a document with an
open derivation (document_embedding_derivations.completed = 0) at has_embedding =
0, so it is re-derived rather than published; the reconciled-count query applies
the same predicate. Test: an open derivation stays unembedded across a reconcile
and flips once its token completes.

Signed-off-by: trvon <me@trevon.dev>

* fix(embedding): require current recipe to skip

Re-derive legacy or mismatched recipes rather than trusting readiness
alone. Treat derivation lookup errors as failed gathering.

Cover service-level recipe changes and legacy readiness, and characterize
atomic batch completion rollback and retry.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* [04/14] Repository hygiene and reviewable configuration/retrieval contracts (#144)

* chore(repo): land the retrieval contract and drop root cruft

Lands work that had been sitting uncommitted in the working tree:

- docs/architecture/retrieval_contracts.md plus its executable form,
  tests/benchmarks/retrieval_contract.py and test_retrieval_contract.py,
  now registered in meson as retrieval_contract_unit (suite: policy).
  .gitignore keeps them tracked beneath the tests/benchmarks/*.py ignore.
- AGENTS.md: the Development Memory section, and the product default
  recorded as hybrid_assist + shadow with topology-off as the control,
  which is what search_topology_stage already does.
  docs/benchmarks/index.md gains the matching search_shadow_cost plan.
- .gitleaks.toml, and a Secret scan workflow that actually runs it on
  push and pull requests (nothing in .github invoked gitleaks before).

Deletes three root files with zero references anywhere in the tree:
bench_profile.json (ad-hoc profiling leftover), CppProperties.json
(Visual Studio config; the repo standardises on Meson), and spdlog.pc
(a hand-copied spdlog 1.12 pkg-config with prefix=/usr/local that would
mislead any PKG_CONFIG_PATH lookup; spdlog comes from Conan).

docs/plans/ (two p2p designs already shipped in 5a53cdfa and d012d95b)
was ingested into YAMS with task=desloppification source=plan, hydrated
via yams cat and byte-compared before removal, per the new AGENTS.md
rule; tests/scripts/analyze_simeon_adaptivity.py no longer points at a
plans directory.

Also reflows two stale line anchors in raw_environment_allowlist.txt
for tools/yams-cli/main.cpp (shifted by 11c39157) so the policy suite
passes again; no new environment writes.

Not included: the external/yams-repo submodule pointer in the working
tree was a stale local clone two commits behind, not an intended bump.

Signed-off-by: trvon <me@trevon.dev>

* test(policy): make the configuration-authority ratchet bidirectional

check_configuration_authority.py only reported keys and TOML readers
present in code but missing from the allowlists, so the reviewed
YAMS_* surface could grow but never shrink: production_environment_keys.txt
even said removals "may remain grandfathered". An allowlist that only
accretes stops describing the code.

The policy now also fails on allowlist entries with no remaining
occurrence in src/, include/, or tools/, naming the lines to delete.
Key removals in later commits therefore have to update the allowlist in
the same change, and the count in the success banner becomes a real
measure of the environment surface (432 today).

stale_entries() exposes the two stale sets for tooling; three unit
tests cover stale key, stale reader, and the returned names.

Signed-off-by: trvon <me@trevon.dev>

* tools(policy): report where every configuration key is referenced

Adds tests/scripts/report_config_key_usage.py: for each YAMS_* key in
production_environment_keys.txt and each dotted TOML key literally
resolved in ConfigResolver.cpp, count whole-token references per tree
bucket (src, test, bench, docs, plugin; build output and the allowlists
themselves excluded) and assign a verdict: KEEP when anything outside
product code exercises or documents the key, CANDIDATE when only
product code mentions it, NOT_IN_SRC when the allowlist is stale.

The report is evidence, not a decision. From here on a key deletion
must paste its CANDIDATE rows into the change description; the first
audit pass hand-listed keys that turned out to have bench and test
consumers, which is exactly the mistake a mechanical inventory avoids.

Today: 432 env keys (215 CANDIDATE, 217 KEEP), 117 TOML keys
(103 CANDIDATE, 14 KEEP). Registered as config_key_usage_report_unit.

The report counts only files git tracks (git ls-files) when the root is a
work tree, so an ignored local bench harness cannot turn a CANDIDATE into a
KEEP on one machine and not another; outside a work tree it falls back to
scanning the directory, which is what the synthetic-tree test exercises.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* [05/14] Consolidate configuration authority (#145)

* refactor(search): retire 45 unreferenced YAMS_SEARCH_* env overlays

LegacySearchConfigEnvironment::applyTo carried 87 YAMS_SEARCH_* overlays
gated behind YAMS_ENABLE_ENV_OVERRIDES. AGENTS.md sanctions them as
test-only benchmark levers, but tests/scripts/report_config_key_usage.py
shows 45 of them are set by no test, no bench arm, no script, and no
doc: they were levers nobody pulled, each shadowing a SearchEngineConfig
field that benches and tests already set directly. The entire
YAMS_SEARCH_GRAPH_* cluster is among them, matching the plan-hygiene
note that graph-vector fusion is a parked lever.

Removed (CANDIDATE rows of the report, verbatim):
  YAMS_SEARCH_BYPASS_CORPUS_WARMING_GATE
  YAMS_SEARCH_ENABLE_GRAPH_FUSION_WINDOW_GUARD
  YAMS_SEARCH_ENABLE_GRAPH_QUERY_EXPANSION
  YAMS_SEARCH_ENABLE_LEXICAL_EXPANSION
  YAMS_SEARCH_ENABLE_WEAK_QUERY_FANOUT_BOOST
  YAMS_SEARCH_FUSION_EVIDENCE_RESCUE_MIN_SCORE
  YAMS_SEARCH_FUSION_EVIDENCE_RESCUE_SLOTS
  YAMS_SEARCH_GRAPH_COMMUNITY_WEIGHT
  YAMS_SEARCH_GRAPH_ENABLE_PATHS
  YAMS_SEARCH_GRAPH_EXPANSION_FTS_PENALTY
  YAMS_SEARCH_GRAPH_EXPANSION_MAX_SEEDS
  YAMS_SEARCH_GRAPH_EXPANSION_MAX_TERMS
  YAMS_SEARCH_GRAPH_EXPANSION_MIN_HITS
  YAMS_SEARCH_GRAPH_EXPANSION_QUERY_NEIGHBOR_K
  YAMS_SEARCH_GRAPH_EXPANSION_QUERY_NEIGHBOR_MIN_SCORE
  YAMS_SEARCH_GRAPH_EXPANSION_VECTOR_PENALTY
  YAMS_SEARCH_GRAPH_FALLBACK_TOP_SIGNAL
  YAMS_SEARCH_GRAPH_FUSION_GUARD_DEPTH_MULTIPLIER
  YAMS_SEARCH_GRAPH_HOP_DECAY
  YAMS_SEARCH_GRAPH_MAX_ADDED_IN_FUSION_WINDOW
  YAMS_SEARCH_GRAPH_MAX_HOPS
  YAMS_SEARCH_GRAPH_MAX_NEIGHBORS
  YAMS_SEARCH_GRAPH_MAX_PATHS
  YAMS_SEARCH_GRAPH_TEXT_MIN_ADMISSION_SCORE
  YAMS_SEARCH_GRAPH_TEXT_WEIGHT
  YAMS_SEARCH_GRAPH_USE_QUERY_CONCEPTS
  YAMS_SEARCH_GRAPH_VECTOR_REQUIRE_BASELINE_TEXT_ANCHORING
  YAMS_SEARCH_GRAPH_VECTOR_REQUIRE_CORROBORATION
  YAMS_SEARCH_GRAPH_VECTOR_REQUIRE_TEXT_ANCHORING
  YAMS_SEARCH_GRAPH_VECTOR_WEIGHT
  YAMS_SEARCH_LEXICAL_EXPANSION_MIN_HITS
  YAMS_SEARCH_LEXICAL_EXPANSION_PENALTY
  YAMS_SEARCH_RERANK_TOP_K
  YAMS_SEARCH_SEMANTIC_RESCUE_MIN_SCORE
  YAMS_SEARCH_STRONG_VECTOR_ONLY_TOP_RANK
  YAMS_SEARCH_TIERED_MIN_CANDIDATES
  YAMS_SEARCH_TIERED_NARROW_VECTOR_SEARCH
  YAMS_SEARCH_TOPOLOGY_FINAL_RESCUE_SLOTS
  YAMS_SEARCH_TOPOLOGY_FUSION_RESCUE_SLOTS
  YAMS_SEARCH_VECTOR_ONLY_NEAR_MISS_PENALTY
  YAMS_SEARCH_VECTOR_ONLY_NEAR_MISS_RESERVE
  YAMS_SEARCH_VECTOR_ONLY_NEAR_MISS_SLACK
  YAMS_SEARCH_WEAK_QUERY_ENTITY_VECTOR_FANOUT_MULTIPLIER
  YAMS_SEARCH_WEAK_QUERY_MIN_TEXT_HITS
  YAMS_SEARCH_WEAK_QUERY_MIN_TOP_TEXT_SCORE

Declared behavior change: YAMS_SEARCH_RERANK_TOP_K and
YAMS_SEARCH_SEMANTIC_RESCUE_MIN_SCORE were older spellings of
YAMS_SEARCH_RERANK_TOPK and YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE
(different clamps, later block won when both were set); only the
current spellings remain. YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS was read by
two identical blocks and is now read once.

A test builds the environment over a recording lookup and asserts none
of the retired names is ever consulted, and that the surviving names
are consulted exactly once. production_environment_keys.txt shrinks by
45 (432 -> 387); the bidirectional ratchet reports 0 new, 0 stale.

The search_latency_graph_rerank task in .vscode/tasks.json set two of the
retired overlays; those assignments are dropped so the task does not claim a
configuration the code no longer honours.

Signed-off-by: trvon <me@trevon.dev>

* refactor(config): drop unread embeddings.* keys and the no-op toggles

yams config embeddings enable/disable/tune wrote embeddings.auto_generate,
embeddings.generation_delay_ms, and embeddings.batch_size, and the
migrator seeded them (plus embeddings.auto_on_add) into every new
config.toml. No daemon code reads any of them: the post-ingest queue
always dispatches embed jobs when an embed queue exists, and the only
batch size the resolvers read is embeddings.runtime.batch_size. The
toggle was a switch wired to nothing, and yams status advised users to
flip it.

- embeddings enable and disable are removed. enable duplicated
  embeddings model <name> once its dead keys are gone; a kept no-op
  would be a shim. status no longer prints an auto-generation line and
  shows the runtime batch size when set. tune presets write
  embeddings.runtime.batch_size and drop the delay.
- config_migration no longer seeds auto_generate, auto_on_add,
  generation_delay_ms, or a top-level batch_size.
- ConfigResolver and the compatibility policy honour a legacy
  [embeddings] batch_size silently when the runtime key is absent, so
  configs in the wild get their value applied without a file rewrite (the
  migrator exits early for v3 configs, so a rewrite would never run).
- status and model hints point at embeddings model <name>.

Declared behavior change: `yams config embeddings enable|disable` are
unknown subcommands now.

Declared behavior change: the legacy [embeddings] batch_size was written by
`yams init` (32), `embeddings tune quality` (8) and `embeddings enable` (16)
but no resolver ever read it. Honouring it as a fallback applies those numbers
for the first time; the one consumer whose result moves is the doctor embedding
repair batch (embeddingPolicy.runtime.batchSize.value_or(8)), which becomes 32
on a stock config. The embedding service already defaulted to 32.

Signed-off-by: trvon <me@trevon.dev>

* feat(config): add [tuning] keys for the setters only env could reach

Seventeen TuneAdvisor setters were exercised by tests but had no TOML
key, so production could reach them only through a deprecated YAMS_*
overlay: connection slot sizing, the CPU-high threshold, the ONNX
concurrency and reservation knobs, model-eviction thresholds, ingest
worker cap, store/embed channel capacities, work-coordinator threads,
and connection lifetime. AGENTS.md says tuning lives in typed config;
these were half-migrated.

Each gets a [tuning] key resolved through ConfigResolver alongside the
existing ones, with provenance recorded, and each is included in
resetConfiguredOverrides() so a reload that drops a key reverts it
(the three with dedicated reset helpers call those). The environment
names keep working as compatibility inputs; nothing new is added to the
YAMS_* surface.

A table-driven test sets all 17 keys, checks every getter, then reloads
with an empty [tuning] section and checks every value reverted.
docs/architecture/environment-overrides.md lists the keys.

The ranged setters (cpu_high_pct 10..100, the three model-evict thresholds
in (0, 1), connection_lifetime_s at most 86400) keep their default when
handed a value outside the range. The resolver checks the same ranges first
and warns, so an ignored value is never recorded as config provenance; test
covers all three shapes.

Signed-off-by: trvon <me@trevon.dev>

* fix(config): report 0 for [tuning] integer keys instead of ignoring it

For the uint32 TuneAdvisor overrides 0 is the unset sentinel and below
every knob's minimum, so `pool_ipc_min = 0` used to be stored, read back
as "unset", and silently fall through to the default while the resolver
still recorded config provenance for it. applyUint32 now warns and skips
the value: nothing changes for the operator except that the mistake is
visible.

Two keys mean something by 0 and are exempt: post_ingest_threads (0 =
auto, which the config migrator seeds into every config and
`yams config` displays as "(auto)") and connection_lifetime_s (0 = no
recycling; its setter takes a signed sentinel and accepts 0, and the
environment spelling always did). Both keep their provenance and stay
silent, so a stock config produces no warning.

Test: pool_ipc_min = 0 and worker_poll_ms = 0 leave the defaults and
record no provenance, pool_ipc_max = 48 still applies, and the two
zero-valid keys apply with provenance.

Signed-off-by: trvon <me@trevon.dev>

* refactor(config): one embedding-runtime resolver, one tuning baseline

Two resolvers read the same five environment keys and three TOML keys
for the embedding runtime with different error semantics:
ConfigResolver::resolveEmbeddingConfig used the strict typed readers
(warn and keep the default), while resolveEmbeddingRuntimePolicy in
config_resolver_policies.cpp used std::stoull inside a bare catch, so
YAMS_EMBED_BATCH=12abc became 12 on one path and a rejected value on
the other. The compatibility entry point is now a projection of the
typed resolution onto the EmbeddingRuntimePolicy shape that library
callers (vector, repair) still consume; it lives in ConfigResolver.cpp
because DaemonConfig is declared there. 91 lines of duplicate parsing
go away.

yams doctor tuning carried its own [tuning] baseline table and it had
drifted: worker_poll_ms 75 against the daemon default of 150,
pool_io_max sized from hardware against a default of 32, so
`doctor tuning --apply` wrote defaults as divergent overrides. The
baseline now reads TuneAdvisor's effective values, which is what
"baseline" should mean.

Tests: the compatibility policy rejects 12abc and agrees with the typed
resolver; the doctor proposal equals TuneAdvisor for every key.

Signed-off-by: trvon <me@trevon.dev>

* refactor(config): stop writing the unread search.reranker_model_path

yams init wrote both search.reranker_model (the name the daemon resolves
through ConfigResolver::resolveRerankerModel) and
search.reranker_model_path, a derived on-disk path nothing in the
daemon, search, or vector code reads; yams config search reranker
status then echoed it back as if it mattered. init no longer writes the
path key and config no longer displays it. The branch that erases a
stale reranker_model_path line from an existing config is kept so old
files converge.

Of the three "aliased key clusters" flagged by the audit, this was the
only real drift: the three embedding-dimension keys are already one
centralized alias set in config_helpers.cpp (read any, write all), and
msl_stack_log_warn_bytes is a metrics counter name, not a config key.

portability_allowlist.txt: three init_command.cpp anchors reflowed; no
new environment reads.

The model command's post-download hint still told operators to set
search.reranker_model_path; that line goes too (the two lines above it
already name the commands that configure the reranker).

Signed-off-by: trvon <me@trevon.dev>

* fix(config): validate tuning ranges and pairs

Reject unsupported scalar and inverted paired limits before changing
runtime overrides or provenance. Preserve explicit zero ONNX reservations
with a distinct unset sentinel and avoid false default-backend conflicts.

Cover all 27 integer keys, inherited limits, and reservation reset.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* [06/14] Measure and reduce repeated SQLite prepares (#146)

* feat(metadata): count uncached prepares in Database::CacheStats

Database::prepare() compiles a statement every call; prepareCached()
reuses one. The cache stats reported hits and misses but nothing about
the bypass, so a hot path that re-prepared fixed SQL on every row was
invisible. CacheStats gains uncachedPrepares, incremented under the
existing cache mutex and carried through move construction and
assignment.

This is the measurement primitive for the following hot-path changes:
a test can warm a path, then assert that N more calls add zero uncached
prepares.

portability_allowlist.txt: one database.cpp anchor reflowed.

Signed-off-by: trvon <me@trevon.dev>

* perf(metadata): cache KG statements and batch the scorer's neighbor fetch

Measured with the new uncachedPrepares counter on a single-connection
pool. Before: 64 warmed neighbors() calls plus 32 document lookups cost
306 uncached statement compilations, and one SimpleKGScorer::score()
over eight query terms cost 620. After: 0 and 1.

Three things were compiling SQL per call on the search path:

- The sqlite KG store used Database::prepare() for fixed SQL in
  neighbors, getDocumentIdByHash/ByPath, ensureBlobNode,
  ensureDocumentNode, ensurePathNode (both lookups), selectKgNodeIdByKey,
  getNodeById, resolveAliasExact/Fuzzy, getEdgesFrom/To,
  getDocEntitiesForDocument, and searchNodesByLabel. All now use
  prepareCached (85 -> 70 raw prepares in the file; the rest are
  dynamic IN-list or maintenance queries).
- ConnectionPool::isConnectionValid ran prepare("SELECT 1") on every
  acquire and again on every release, so each pooled call paid two
  compilations before doing any work. The probe is now cached.
- Database::tableExists compiled its sqlite_master probe every time;
  the fuzzy alias resolver calls it per query token.

SimpleKGScorer builds its 1-hop neighbor union with one
getEdgesFromBatch call instead of neighbors() per query node.

kMaxCacheSize rises from 64 to 128: 90 cached call sites now share a
connection when KG and repository work interleave, and the cache
evicts first-found, not least-recently-used.

Tests pin both numbers: the store test asserts zero uncached prepares
across 96 warmed lookups; the scorer test asserts fewer than eight on
a second run (previously one per query node from neighbors() alone).
portability_allowlist.txt: two anchors reflowed.

Signed-off-by: trvon <me@trevon.dev>

* fix(metadata): bound batched edge query parameters

Respect connection-specific SQLite limits and deduplicate chunk inputs.
Preserve directional ranking and a single read snapshot across chunks;
roll back the savepoint on query failure.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* [07/14] Batch tag and topology hydration (#147)

* perf(search): load tag filters once per request, not once per document

pathSearch and searchByHashPrefix filtered candidates through
metadataHasTags(), which for every document called getAllMetadata()
(four retries with backoff on failure), concatenated every metadata
key and value into a string, and logged that string at info level.
A path search over a few thousand documents was a few thousand
metadata round trips and a few thousand info lines.

Both paths now load tags for the whole candidate set with one
batchGetDocumentTags() call (the API GraphComponent already used) and
filter with a pure matcher. The legacy fallback that also accepted a
metadata row with key "tag" is gone: nothing writes that form and the
repository's own tag APIs never honoured it, so search and repository
now agree on what a tag is.

A counting repository subclass pins the shape: one batch call and zero
getAllMetadata calls for both the path and the hash-prefix routes.
portability_allowlist.txt: two search_service.cpp anchors reflowed.

Correction to the claim above that nothing writes the legacy `key = "tag"`
form: the repair downloads pass did (tag values "downloaded", "host:<h>",
"scheme:<s>"), and the search fallback was its only reader. That writer now
uses the same `tag:<name>` keys as every other tag writer, so `--tags
downloaded` keeps matching documents the repair pass tagged. And
batchGetDocumentTags chunks its IN list (500 ids), since pathSearch feeds it
an unbounded candidate set; an oversized list used to fail the statement
and, with this change, would have emptied the result instead of degrading
per document.

Signed-off-by: trvon <me@trevon.dev>

* perf(search): hydrate topology routing candidates in batches

The routing session called getDocumentByHash once per hash in three
places on the product routing path: ranked-candidate admission, cluster
expansion, and the graph-neighbor diagnostic trace (memoized, but still
one statement per distinct hash across four stages). Each now issues
one batchGetDocumentsByHash for its whole set, the same API the
topology input extractor already used, and the docLookupMicros timing
covers the batch. Cluster expansion applies the maxDocs cut before
hydrating so no document is fetched only to be discarded.

A session-level test with one seed and eight graph neighbours under a
counting repository fails on the previous code (17 single lookups, no
batch) and passes now (0 single lookups). The full topology suite is
unchanged.

portability_allowlist.txt: one topology test anchor reflowed.

Signed-off-by: trvon <me@trevon.dev>

* fix(search): bound hydration and preserve errors

Retain legacy tags and honor per-connection SQLite bind limits.
Keep hash chunks in one read snapshot and release it on failure.

Failed lookups are not missing documents or successful empty searches.
Reject partial route admission and preserve tagged path errors.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* [08/14] Bound retained corpus and enrichment resources (#148)

* perf(search): hold Simeon concept mining to the corpus byte budget

The BM25 pass streams one chunked document at a time and stops when
processed text would exceed max_corpus_bytes. The concept-mining pass
that follows re-read every document's raw text into one vector and
kept it all resident for mine_concepts(), with no budget at all: on a
large corpus the build's peak memory was the whole corpus, regardless
of the cap the first pass had just enforced. This is the likeliest
single contributor to the daemon's multi-gigabyte footprint.

The concept pass now applies the same budget to raw bytes as it loads.
When the corpus exceeds it, mining is skipped with a warning and the
BM25 index is kept; the partially loaded text is released. A corpus
within budget is mined exactly as before.

Test: 120 documents of 4 KiB with a chunk cap that keeps the BM25 pass
under 64 KiB. Over budget, the build completes with zero concepts
(previously 81 were mined from ~480 KiB of resident text); with a
4 MiB budget the same corpus mines concepts.

Signed-off-by: trvon <me@trevon.dev>

* perf(ingest): bound the pending KG job overflow

When the bounded kg_jobs channel (16384 slots) is full, enqueueKgJob
parks jobs in pendingKgJobs_, a std::deque with no limit. Each KgJob
can carry the document's content bytes, so a stalled or slow KG stage
turned into unbounded ingest memory on top of the ~36k entries the
symbol, entity and KG channels already hold.

The overflow FIFO now has a cap, tuning.post_ingest_pending_kg_max
(typed key on TuneAdvisor, default 16384, no environment twin per
AGENTS.md). At the cap the job is dropped: the document's KG stage is skipped for
this ingest, the drop is counted through the existing kgDropped bus
counter (shared with shutdown cancellations), and a throttled warning
names the cap. Nothing re-derives the per-document entity graph for a
dropped job today; the structural repair pass rebuilds document nodes
and orphans but not that stage, so an operator who sees the warning
should raise the cap or re-ingest. Bounded memory with a visible drop
beats unbounded memory with an invisible stall.

Test: with the channel detached and a live coordinator (so the drain
coroutine neither cancels nor consumes), three enqueues against a cap
of two leave two pending and one dropped; before the cap all three
were pending.

Signed-off-by: trvon <me@trevon.dev>

* fix(ingest): preserve durable KG admission state

Persist pending generation with content; acknowledge only matching committed\nKG work, never a dropped queue item or legacy submission-time state.\nKeep processing state local to each receiver and release queued raw bytes.\n\nC++ job and batch structs gain fields; rebuild consumers. Pending work\nis observable but automatic retry is not introduced.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* [09/14] Bound rerank text and reuse search parsing (#149)

* perf(search): preview-based rerank text and compiled path globs

Cross-rerank built each window entry's text with a full getContent()
read, then kept only the first rerankSnippetMaxChars characters. The
window now hydrates with one batchGetContentPreview() call bounded to
that many characters; buildCrossRerankText takes the preview instead of
the repository. batchGetContentPreview is virtual so a test can count
it: three window results cost one preview batch and zero full reads.

Path filtering compiled a std::regex from the glob on every call, and
the call sits inside per-document loops in pathSearch, the hash-prefix
search, and metadata filtering. GlobMatcher (src/app/services) holds
the compiled form; wildcardMatch() reuses it through a small
per-thread cache. The glob semantics are unchanged and now pinned by a
test: '*' stays within one path segment, '**' crosses, '?' is one
character, regex metacharacters are literal. yams::common::wildcard_match
was considered and rejected because its '*' crosses separators.

portability_allowlist.txt: two search_service.cpp anchors reflowed.

Signed-off-by: trvon <me@trevon.dev>

* perf(search): parse edge anchor properties once, lower the path query once

Graph expansion parsed every visited edge's JSON properties with
nlohmann::json::parse to read at most two strings, region and scope,
in three places (relation weighting, seed anchoring, and neighbour
anchoring). Properties strings repeat heavily across edges and most
carry no anchor at all. edgeAnchorHints() now returns immediately when
the text cannot contain either key and otherwise parses each distinct
string once per thread through a small bounded cache. A process-wide
parse counter lets the test pin it: one hundred lookups of the same
anchored string cost one parse, and un-anchored or malformed strings
cost none.

computePathMatchScore lowercased and trimmed the query for every
candidate document; pathSearch now does that once and passes the
lowered query down. The existing path-search tests cover the scoring.

portability_allowlist.txt: two search_service.cpp anchors reflowed.

edgeAnchorHints returns its two short strings by value: the per-thread memo
clears itself at 1024 entries, so a reference into it could dangle across
calls even though today's three callers consume the result immediately.

Signed-off-by: trvon <me@trevon.dev>

* fix(search): preserve hydration failure semantics

Stop reranking before scorer calls or score changes when previews fail.
Commit anchor hints atomically and recognize escaped JSON keys.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* [10/14] Move large inline implementations into translation units (#150)

* test(memory-sync): give the CLI fixture the shared corpus scope it needs

94e948c6 made createMemorySyncService fail closed unless
memory_sync.corpus_scope is "shared" and updated the service-manager
test, but not the p2p CLI fixture, whose eight cases have failed at
construction ever since ("replication requires
memory_sync.corpus_scope=shared"). The fixture now sets the scope; the
cases run again and pass.

Signed-off-by: trvon <me@trevon.dev>

* refactor(memory_sync): move MemorySyncLoop bodies into a translation unit

include/yams/memory_sync/memory_sync.h was a 3,527-line header-only
class: 120 inline method definitions, 2,900 lines of implementation,
pulled into every translation unit that includes daemon.h or the p2p
protocol header. The memory-sync subsystem had no source directory at
all.

MemorySyncLoop's bodies now live in src/memory_sync/memory_sync.cpp,
compiled once into a new yams_memory_sync static library that follows
the yams_wal pattern. The header keeps the declarations, the nested
types, the constructor, one-line accessors, and the YAMS_TESTING hooks
(those must stay inline because the library is not built with the
testing define). The daemon library and its declared dependency link
the new library, as do the five memory-sync test binaries.

Zero semantic change: the move is mechanical (a scripted split that
copies each body verbatim, drops default arguments and static from the
definitions, and qualifies nested return types). All seven memory-sync
test binaries and the service-manager suite pass unchanged.

memory_sync.h: 3,527 -> 569 lines.

Signed-off-by: trvon <me@trevon.dev>

* refactor(daemon): move TuneAdvisor method bodies out of the header

TuneAdvisor.h was 3,345 lines, almost all of it the bodies of ~300
static accessors and setters, included by 60 translation units. Each
of them recompiled the whole policy surface on every touch, and any
change to one knob rebuilt most of the daemon.

The bodies now live in src/daemon/components/TuneAdvisor.cpp (which
previously held only the system-memory probe). The header keeps the
declarations, the static inline override slots (unchanged, so the
ODR story for process-wide tuning state is exactly what it was), the
YAMS_TESTING hooks, and the two methods whose bodies are
preprocessor-conditional on YAMS_TESTING; those must stay inline
because the library is not built with the testing define and moving
them would silently switch tests to the production branch.

Mechanical move by the same scripted split used for memory_sync.h:
bodies copied verbatim, static and default arguments dropped from the
definitions, nested return types qualified. The one template
(readConfiguredOverridesSnapshot) stays in the header because three
other translation units instantiate it.

The header is included well outside src/daemon (vector, repair, app
services, MCP, the CLI doctor, the ONNX plugin), and those targets do
not link yams_daemon. They used to get the bodies for free from the
header, so the move needs a home they can all link: the new
yams_tune_advisor static library (src/daemon/tune_advisor/meson.build)
builds TuneAdvisor.cpp alone, and yams_daemon, yams_vector,
yams_repair, yams_app_services, yams_mcp, yams_cli, the ONNX plugin,
and the standalone snapshot test depend on it.

Executables compiled with -DYAMS_TESTING link a second build of the
same file, yams_tune_advisor_testing, ahead of the production archive.
Under that define compatibilityEnvironment() reads the process
environment live instead of the startup snapshot, which every
env-driven tuning test relies on; before the split each such TU
instantiated the inline bodies itself with the define. Meson emits
dependencies in list order and the first archive defining a symbol
wins, so the order is load-bearing: test_deps, the benchmark
dependency lists, and the mobile ABI test all put the testing archive
first, and tests/meson.build says so. The testing archive is built
under the same predicate that enters tests/ (a plain debug configure
included), so `meson setup --buildtype=debug` keeps working. The
standalone snapshot test keeps the production build on purpose.

No behavior change; a full debug compile is clean and the tuning
suites pass unchanged.

TuneAdvisor.h: 3,345 -> 1,170 lines.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* [11/14] Separate status presentation and centralize shared query helpers (#151)

* refactor(daemon): one table for repair operation wire codes

The code published under repair_current_operation_code was defined in
three places: a name->code chain in RepairService.cpp, an enum->name
switch beside it, and a code->label switch in the CLI. The two daemon
tables agreed with each other but the CLI copy had already drifted in
wording, and nothing tied the three together.

metric_keys.h now carries the single code<->name table next to the
metric key it describes, with constexpr lookups in both directions.
RepairService's name->code chain delegates to it; the enum->name
switch stays because it is keyed by the private enum. The CLI copy
goes in the next commit with the status renderer.

Signed-off-by: trvon <me@trevon.dev>

* refactor(cli): extract the daemon status renderer

showStatus() in daemon_command.cpp was 1,762 lines: two RPCs, a
bootstrap-file fallback, and two ~400/1,100-line render bodies that
wrote straight to std::cout from inside the command class, reachable
only through a live daemon. The render bodies read nothing from the
command except the expected data directory, the explicit-socket flag,
and a memory-sync fetch they triggered mid-render.

The bodies now live in src/cli/commands/daemon_status_render.cpp as
free functions over StatusResponse plus a small DaemonStatusRenderContext
(configured data dir, explicit-socket flag, optional MemorySyncResponse)
writing to an ostream. The readiness/label helpers that only they used
(humanizeToken, classifyReadiness, isEphemeralDataDir,
daemonDataDirFromStatus, paintStatus, neutralText) move with them.
DaemonCommand keeps the RPCs and the bootstrap fallback; the memory-sync
fetch becomes fetchMemorySyncStatus() and runs just before rendering
instead of in the middle of it, so the section prints in the same place
with the same bounded 3 s timeout.

The move is verbatim: after the mechanical substitutions
(std::cout -> os, expectedDataDir() -> ctx.configuredDataDir, the
memory-sync call, the explicit-socket flag, and the repair label
function) the two bodies match the originals character for character
modulo whitespace. The CLI's private repair-operation switch is gone;
the label now comes from the shared table in metric_keys.h with
underscores rendered as spaces, which reproduces the previous wording
("stuck docs", "block refs", "path tree").

tests/unit/cli/daemon_status_render_catch2_test.cpp drives both
renderers and the memory-sync section from synthetic responses:
overview rows, blockers with init progress, last error, data-dir
mismatch (and its suppression under --socket), temporary data dir,
the active repair operation label, every detailed section, and the
started/not-started memory-sync gate.

daemon_command.cpp: 4,082 -> 2,341 lines; daemon_status_render.cpp:
1,642 lines.

Signed-off-by: trvon <me@trevon.dev>

* refactor(app): one home for hash predicates, path aliasing, and metadata qualifiers

Three small helpers had grown parallel copies with quietly different
rules:

- "does this look like a hash" existed four times: search_service
  (8-64 hex), document_service (6-63), cat_command (6-64), plus a
  never-defined utils::looksLikeHash declaration in services.hpp. The
  8 vs 6 threshold is deliberate (D3): a bare query token must reach
  8 hex characters before it stops being text, while an explicit hash
  argument may be 6. yams/common/hash_predicates.h now names the two
  predicates apart (looksLikeHashQueryToken, looksLikePartialHashArgument)
  over one isHexDigits; document_service's 6..63 bound was deliberate
  for the name resolver (a full 64-hex name must not resolve as a prefix
  and delete by hash), so its two resolver sites now guard size() != 64
  explicitly, as the partial-hash resolution site already did.

- macOS /private aliasing had three incompatible policies: search
  appended the alias of /var only, grep canonicalized /var and /tmp
  to the /private spelling, and the delete resolver displayed
  /private/var as /var. yams/common/fs_utils.h now carries one alias
  table ({/var,/tmp} <-> /private/...) with three lexical policies over
  it (otherSpelling, canonicalSpelling, displaySpelling) and the
  platform-gated wrappers callers use. Declared behavior change, macOS
  only: search patterns under /tmp now also match their /private/tmp
  spelling, and the resolver displays /private/tmp/... as /tmp/....

- Inline key=value metadata filters were parsed twice: SearchService's
  extractStructuredMetadataQuery (exactly one '=', no quoting/grouping
  characters, deduplicated by key) and SearchEngine's
  parseMetadataFiltersFromQuery (any '=' token, duplicates kept). The
  stricter, tested parser moves into yams/search/query_qualifiers.hpp
  and both call it. Declared behavior change for the hybrid metadata
  component: tokens like name="x" or a=b=c stay in the query instead
  of becoming filters, and repeated keys collapse to the last value.

Tests: hash_predicates_catch2_test.cpp pins both thresholds and the
64-character bound; path_alias_catch2_test.cpp pins the alias table,
the three policies, and the platform gating; query_qualifiers_catch2_test.cpp
gains the structured-metadata cases (split, dedupe, malformed tokens,
filters-only). search_service, document_service, grep_service,
service_utilities, and resolve_submodule pass unchanged. Portability
allowlist anchors reflowed for the four files whose lines moved.

Signed-off-by: trvon <me@trevon.dev>

* fix(cli): flush status before optional collection

Render useful primary status before waiting for memory-sync details.
Keep both brief and detailed paths behind the tested ordering seam.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* fix: fence enrichment completion and reconcile promotion prerequisites (#157)

* test(ci): stabilize main matrix (#112)

Signed-off-by: trvon <me@trevon.dev>

* ci(release): preview experimental promotion (#111)

Signed-off-by: trvon <me@trevon.dev>

* ci(release): reconcile draft promotion safely (#113)

* ci(release): reconcile draft promotion

Create promotion visibility independently of release readiness.
Leave existing PRs untouched to preserve concurrent maintainer edits.
Validate against current main without masking candidate rejection.

Signed-off-by: trvon <me@trevon.dev>

* ci: isolate hook Git environments

Prevent temporary Git fixtures from modifying the calling worktree.
Clear repository-local hook variables before the unchanged build gates,
and isolate validator and fixture Git subprocesses from caller overrides.
Exercise linked-worktree config, ref and index preservation offline.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* fix(ci): reject invalid candidate evidence

Explain missing main history without mutating refs or weakening promotion validation. Match experiment boolean and aggregation parsing, respect candidate-limit precedence, and return issues for malformed configuration or metric values.

Signed-off-by: trvon <me@trevon.dev>

* fix: preserve completion and retrieval contracts

Propagate derivation SQL failures, invalidate KG status caches, and avoid unused preview reads. Align explicit hash validation and export standalone client dependencies. Keep secret exceptions fixture-scoped and remove undefined behavior from status fixtures.

Signed-off-by: trvon <me@trevon.dev>

* fix: fence asynchronous enrichment completion

Coordinate graph and NL commits before acknowledging enrichment. Atomically classify embedding reuse, fence repair status writes, and preserve newer vectors on old failures. Upgrade content readiness triggers and keep repository counters coherent. Six affected ASan suites pass; this does not provide restart replay or generation-scoped vector storage.

Signed-off-by: trvon <me@trevon.dev>

* fix: bound semantic graph output reservation

Use retained neighbors for admission and node capacity instead of mixing corpus and pair counts. Preserve single-neighbor output and avoid quadratic reservation. Characterization regression fails three assertions before the fix; full component ASan suite passes afterward.

Signed-off-by: trvon <me@trevon.dev>

* fix: fence standalone embedding repair completion

Mint attempt tokens before reading canonical repair input and publish only current persisted document embeddings. Do not skip stale physical vectors. Make SQLite preparation fault injection connection-local and deterministic. Full component and metadata ASan suites pass.

Signed-off-by: trvon <me@trevon.dev>

* fix: preserve enrichment failure reporting

Admit pending embedding attempts before provider failures while preserving completed results. Route title no-op acknowledgement through the writer so flush reports SQL failures and duplicate jobs can retry. Four affected full ASan suites pass; host configuration hashes unchanged.

Signed-off-by: trvon <me@trevon.dev>

* test: align explicit hash prefix boundaries

Reject five characters and accept the supported six-character minimum. Full hybrid_search suite passes. Separate daemon shutdown sanitizer failure remains under investigation despite a passing targeted rerun.

Signed-off-by: trvon <me@trevon.dev>

* test: align UI hash validation boundary

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>

* test: harden release staging coverage

Scope vector fixture overrides across every disable alias and verify restoration. Prune excluded repository directories before walking them. Exercise CLI serve delegation and distinguish outer spawn failure from missing child diagnostics using isolated stubs.

Signed-off-by: trvon <me@trevon.dev>

* test: honor memory sync timeout scaling

Apply the existing configured timeout scale to the integration executable. Observe periodic backfill attempts while retaining all publication assertions; no forced backfill or production scheduling change. Full TSan memory-sync suite passed.

Signed-off-by: trvon <me@trevon.dev>

* fix(ci): repair audited platform regressions

Prune CI artifacts and stream metadata scans. Preserve relocated CLI
loader dependencies, reject incomparable path roots, and make deadline
and corruption fixtures test their stated contracts.

Keep unresolved Windows publication and hook failures diagnostic-only.

Signed-off-by: trvon <me@trevon.dev>

* fix: preserve cancellation across worker restart

Reassert backend cancellation under the lifecycle lock. Protect fake
backend condition-variable predicates against lost wakeups.

Fix Windows fixture encoding, shell selection, and rooted-path checks.
Keep the unresolved rename failure diagnostic-only.

Signed-off-by: trvon <me@trevon.dev>

* fix(metadata): reserve pool creation capacity

Reserve slots before unlocking for connection creation. Route replacement
and maintenance through the same capacity accounting and wake waiters
when reservations settle.

Reproduce concurrent over-admission with bounded creation barriers.
Reanchor existing portability exceptions without adding exemptions.

Signed-off-by: trvon <me@trevon.dev>

* test(daemon): synchronize on committed content state

PostIngestQueue::processed() counts prepared tasks before their content is
committed, so waiting on it and then reading repository state is racy.
Wait on the committed observable in the three affected cases and
characterize the counter ordering with a gated-insert regression.

Signed-off-by: trvon <me@trevon.dev>

* fix(storage): scope Windows long paths to paths that need them

Add the extended Win32 prefix per filesystem operation and only when
the path reaches MAX_PATH, instead of rewriting the whole storage root.
Short paths keep their ordinary spelling, so listing, canonicalisation
and iteration semantics are unchanged.

Derive list() keys lexically from de-prefixed entries, and canonicalise
containment in the extended namespace but compare it in the ordinary one,
so prefixed and plain spellings are never mixed.

Keep the existing publication regression and cover drive/UNC spelling
plus long-key CRUD.

Signed-off-by: trvon <me@trevon.dev>

* fix(storage): enumerate Windows long paths in the extended namespace

Directory enumeration builds descendant paths internally, so a short
root can still produce child paths past MAX_PATH and fail partway
through the walk. Run list() and getStats() iteration in the extended
namespace unconditionally on Windows while keeping key derivation on
de-prefixed paths.

Found by the hosted Windows lane: the long-key listing case failed at
REQUIRE(listed) with the backend returning a filesystem error.

Signed-off-by: trvon <me@trevon.dev>

* test(daemon): keep repair work pending while awaiting degrade

The lifecycle Degraded transition requires a non-empty repair queue at
the moment the busy hold elapses (pending && busyHeld). A finite batch
that drains faster than the hold returns the daemon to Healthy, so
enqueue-once-then-wait is racy against machine speed and fails
independently of load. Resubmit bounded work while waiting.

Observed on hosted linux-arm64 and linux-x86_64 in runs containing no
Windows-path change, so it is not caused by the storage work.

Signed-off-by: trvon <me@trevon.dev>

* fix(wal): keep WAL metrics noexcept and race-free

getStats() is noexcept but calls a WALManager query that iterates the
WAL directory, so an inaccessible directory terminated the daemon
instead of degrading to zeroed metrics. Catch and report zeros.

WALManager::getStats() also read lastSync/lastRotation after releasing
stateMutex while the sync and rotation paths write them under it. Read
them inside the same critical section.

Signed-off-by: trvon <me@trevon.dev>

* fix(storage): harden reference-counter schema migration errors

Stop discarding the failure text, and stop swallowing non-duplicate
errors in the ALTER handler. Lock contention during migrations stays
classifiable and retryable, and the schema can no longer be treated as
migrated when the ALTER failed for an unrelated reason.

No observed abort is reproduced by this change: the pre-existing
classifier already retried the held-lock case, because the failure
surfaces with a SQLITE_BUSY-style message. This is hardening; the added
contention test is a regression guard, not a red/green proof.

Signed-off-by: trvon <me@trevon.dev>

* fix(ipc): validate memory sync operations against a named wire limit

The template deserializer bounded operations by Enroll, so appending an
enum member would reject the new value. Use one named constant tied to
the highest member.

This is a latent path: it has no callers. The shipping protobuf path is
unaffected because proto_serializer.cpp asserts the proto range is >=
the highest C++ value rather than equal to it, and the round-trip test
added here covers protobuf rather than the bound.

Signed-off-by: trvon <me@trevon.dev>

* fix(vector): default the paging capability instead of requiring it

getVectorsPage was introduced pure virtual, which is source-breaking for
downstream IVectorStore implementations. Give it a NotSupported default.

The one in-tree implementer already overrode it, so this protects
out-of-tree stores rather than repairing a break here. Note that
IStorageBackend::listPage is a working compatibility implementation,
whereas this default reports NotSupported, so callers that treat a
message as a plain error cannot distinguish it from a real failure.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <git@trevon.dev>
Signed-off-by: trvon <me@trevon.dev>
Co-authored-by: trvon <git@trevon.dev>
Co-authored-by: trvon <me@trevon.dev>
Co-authored-by: 0xkite <96210987+0xkite@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-19 16:27:50 -06:00