Skip to content

Dfaconti/cloud - #1

Merged
facontidavide merged 46 commits into
mainfrom
dfaconti/cloud
Jul 13, 2026
Merged

Dfaconti/cloud#1
facontidavide merged 46 commits into
mainfrom
dfaconti/cloud

Conversation

@facontidavide

Copy link
Copy Markdown
Contributor

No description provided.

GNERSIS and others added 30 commits June 12, 2026 22:45
…osal, handbook

The canonical design spec (§1–14), Plan A (Go server), Plan B (C++ client), Plan C
(integration harness), Plan D (PJ4 plugin), the unified Dexory-S3 + Asensus-GCS
plan, the commercial proposal + overview, the M1 reports/runbook/closure, the
full-plan audit, the toolbox parser-delegation plan + walkthrough, the project
handbook (CLAUDE.md), README, and the architecture overview.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vendored PlotJuggler 4 app + official plugin collection (synced to upstream),
carrying the connector's host-side changes (parser-ingest tail slots, RangeSlider
markers, widget bindings, FrameTransforms 3D ingest) and the toolbox_dexory_cloud
connector plugin + the Qt-free dexory-cloud-cli. Build outputs (.qt, build/) are
gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single-binary Go server (catalog, indexer, session streaming with latched
replay, storage seams S3+GCS, format codec, authn, dashboard/metrics), the
canonical wire proto + checked-in Go bindings, the Minio + fake-gcs infra, the
headless smoke/matrix harness + dev tooling, one-command build.sh/run.sh
bring-up, and the GitHub CI workflows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the plain-source vendored PJ4/ tree with version-pinned PRIVATE fork
submodules (PJ4-cloud + nested plotjuggler_sdk-cloud, pj-official-plugins
pristine-or-thin-fork), relocate the toolbox_dexory_cloud connector plugin into
pj-mcap-server/plugin/, and adopt a git-merge upstream-sync workflow. Approved
design; implementation plan to follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task-by-task plan implementing the 2026-06-12 design spec: create the private
SDK + PJ4 forks (nested), relocate toolbox_dexory_cloud into plugin/, rewire
pj-mcap-server submodules, retarget the build, and gate on smoke + matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the connector plugin out of the vendored plugins tree to plugin/, beside
the proto it consumes. It now builds STANDALONE against the plotjuggler_sdk
Conan package (added the project/scaffolding + include(CTest) the plugins-repo
parent used to provide). Vendor its two cmake helpers + the Foxglove MCAP
writer; rebind the proto path (../../proto), MCAP path (third_party), and the
SDK_VERSION pin (plugin/SDK_VERSION). Builds clean; ctest 33/33; CLI links no Qt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PJ4/ -> PlotJuggler/PJ4-cloud@cloud (89b0b11), with nested plotjuggler_sdk ->
PlotJuggler/plotjuggler_sdk-cloud@cloud (17d946d); pj-official-plugins/ (now a
SIBLING of PJ4/) -> PlotJuggler/pj-official-plugins-cloud@cloud (1d3b584). All
three forks are PRIVATE. git submodule status now records the exact
upstream-tracked commit; sync is 'git fetch upstream && git merge <tag>' on the
fork's cloud branch (see the design spec dated 2026-06-12).

build.sh/run.sh/smoke.sh/matrix.sh retargeted: the connector builds standalone
from plugin/toolbox_dexory_cloud, the official plugins from the sibling submodule,
the SDK 0.7.1 package is conan-created from PJ4/plotjuggler_sdk. Verified:
SMOKE PASS, MATRIX PASS (8 legs incl. GCS), full ./build.sh green (app + .so staged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…BOOK)

Describe the private fork-submodule layout, the per-fork upstream bases, the
git-merge sync workflow, the _pre_rewire/_pre_submodule rollback tags, and the
relocated connector at plugin/toolbox_dexory_cloud. Update build/run/CLI paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump each private fork's cloud branch to merge the latest upstream and re-point:
  PJ4-cloud            defcb0a  (upstream a19d49e: Qt 6.8.3->6.11.1 + 24 commits, our 18 host hooks merged clean)
  plotjuggler_sdk-cloud 94b57b2 (upstream 8f485e5 v0.8.0: PosesInFrame + time spine; our tail slots -> 0.8.1, ABI sentinels pass)
  pj-official-plugins-cloud e2d975e (upstream bb0ebd5: PosesInFrame parser; SDK_VERSION -> 0.8.1, GCC demotion kept)
Connector SDK pin -> 0.8.1 (no code change needed). build.sh now derives the SDK
version from the recipe and gates the app on Qt 6.11.1 (was a stale 6.8.3 guard).

Verified on the synced stack: clean ./build.sh (app links Qt 6.11.1), SMOKE PASS,
MATRIX PASS (8 legs incl. GCS). All three forks pushed; merge conflicts were only
version strings + the nested SDK pointer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rop stale docs

- Move the design/plan markdown to arch/ and the rendered HTML to docs/;
  keep CLAUDE.md + README.md at root. Fix all references (CLAUDE.md, README,
  _render_proposal.py now reads arch/ + writes docs/).
- Add docs/2026-06-15-how-it-works-explained.html: a plain-English, diagram-led
  architecture walkthrough focused on metadata indexing, filtering, and behaviour
  at ~8M recordings, using real Dexory (Hive-partitioned) metadata as examples.
- Trim arch/ to the 8 canonical design docs: remove 2 stale explanations
  (superseded/outdated walkthroughs) + 5 completed-milestone reports/plans
  (M1 report/closure/runbook, full-plan audit, Slice-16 + vendoring plans).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0.11.0

Repo now focuses on server + plugin only. PJ4 and pj-official-plugins are
managed externally (PlotJuggler/PJ4-cloud and PlotJuggler/pj-official-plugins-cloud).

- Remove PJ4 and pj-official-plugins git submodules
- build.sh: drop SDK-from-submodule and PJ4 app build steps (2 steps → 2 steps)
  Callers must have plotjuggler_sdk in their Conan cache before building the plugin
- run.sh: drop PJ4 GUI launch; backend URL is always printed to stdout
- plugin/SDK_VERSION: bump 0.8.1 → 0.11.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SDK 0.11.0 extracted parser-ingest into its own ParserIngestHostView class
(previously shared DataSourceRuntimeHostView). Update the ingest_ member
type and its reset in finalize() — call sites are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The third_party/mcap tree was a manual copy of the Foxglove MCAP C++ library.
Replace it with the official Conan package — same #include <mcap/writer.hpp>
path, no source tree to maintain.

- conanfile.py: add mcap/2.1.1; align zstd→1.5.5 and lz4→1.9.4 to mcap's
  transitive pins (no functional change to what we decompress)
- CMakeLists.txt: add find_package(mcap) + link mcap::mcap to dexory-cloud-cli;
  remove DEXORY_CLOUD_VENDORED_MCAP SYSTEM include
- delete plugin/toolbox_dexory_cloud/third_party/mcap/ (19 files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
README no longer references PJ4/pj-official-plugins (removed in the previous
commit) or the GUI launch step. Layout table and quickstart reflect the current
server + plugin only structure.

Drop docs/ entirely — both HTML files were generated artifacts from before the
repo was narrowed to server + plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds AurynRobotics/mcap_server (the Python mcap_catalog_builder) as the mcap_catalog/ submodule, pinned at auryn main. It is the sole-writer side of the catalog migration (docs/auryn-catalog-migration-plan.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion specs

arch/ = archived, docs/ = current. Delete 5 spent implementation plans (Plan A/B/C/D + two-endpoints; recoverable from git). Add docs/{auryn-catalog-migration-plan,catalog-vocabulary-rpc,gce-deploy-smoke,dexory-staging-bucket-notes}.md. Prune CLAUDE.md dangling refs + add a current-focus/execution entry point.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`conan install` writes conanbuild.sh / conanrun.sh / conan*env-*.sh /
deactivate_conan*.sh to plugin/toolbox_dexory_cloud/ (the plugin root,
not build/), so the existing build/ ignore rule missed them and they
showed up as untracked. Pure generated build artifacts — never source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auryn catalog migration Milestone 1 (plan §1.0/§1.2/§2.1/§5.1): the smallest
end-to-end slice proving the cross-language contract before the M2 query rewrite.
A Go server reads an auryn-(Python-)written DB read-only on regenerated Hive
fixtures. Reviewed by Claude (adversarial multi-lens) + Codex; all findings fixed.

- catalog.OpenReadOnly (readonly.go): opens mode=ro, does NOT apply the Go schema
  (the auryn schema is authoritative), verifies schema_version == SchemaVersion(1)
  and fails fast otherwise (missing table/row or wrong version). Store gained a
  readOnly flag: Write returns ErrReadOnly; Close skips the (absent) writer
  goroutine without touching nil channels.
- gen-ci-fixtures -hive: additive Hive-key layout
  (customer=/customer_site=/robot=/source=/date=/<name>.mcap) so the auryn builder
  extracts dimensions. Default OFF — the flat seed/smoke path is byte-unchanged.
- CATALOG_CONTRACT.md: the cross-language change-control doc (identical to the
  submodule copy).
- Tests: readonly_test.go (happy/mismatch/unstamped/missing-file, hermetic) and a
  build-tagged `crosslang` proof (Python builds -> Go reads -> schema_version +
  dimensions). The default `go test ./...` stays 100% hermetic (no python dep).
- mcap_catalog: bump to the M1 builder commit (schema_version, --once, foreign-DB
  guard, contract doc).

Verified: go test ./... (hermetic) + -race + crosslang all green; Python 99 tests;
real-data check (builder cataloged 2 real 171-channel rosbox MCAPs from the Dexory
staging bucket, Go read them back). Live serving path untouched (main.go still uses
catalog.Open — the cutover is M2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auryn catalog migration Milestone 2 (reader half). The Go server can now serve
fully from the auryn (Python-builder) schema; the live serving path is UNCHANGED
(still catalog.Open + the Go indexer) — the cutover is M6. Reviewed by Claude
(adversarial multi-lens) + Codex; both GO-WITH-FIXES, all findings fixed.

- auryn_read.go: the read-only query side over the auryn schema —
  aurynFilterFiles / aurynGetFile / aurynListTopicsForFile / aurynHasHierarchicalKey,
  with decodeCountsBlob (unsigned LEB128, byte-identical to varint.py), sumCounts,
  and rebuildHiveKey (exact inverse of keyparse.rebuild_hive_key, D1 — no stored
  s3_key column). message_count is summed from topic_counts; topics are
  reconstructed from the deduped topic_set, zipped topic_id-ASC then name-sorted.
- The public FilterFiles/GetFile/ListTopicsForFile/HasHierarchicalKey now branch
  `if s.readOnly { return aurynX(...) }`, so the WS handlers are untouched and the
  M6 cutover is a one-line catalog.Open -> OpenReadOnly flip in main.go. The
  legacy Go-schema queries remain for the still-live indexer path.
- const SchemaVersion = 2 (lockstep with the Python SCHEMA_VERSION).
- Review fixes: aurynListTopicsForFile fails fast on a topic_counts/topic_set
  cardinality mismatch (Codex — no silent mis-count); the Go schema's
  tags_effective view aligned to UNION ALL to match the auryn schema (Claude);
  CATALOG_CONTRACT.md reader-reads list corrected (has_error reserved-not-read,
  deduped topic_set_id).
- Tests: auryn_read_test.go (hermetic in-Go auryn DB — FilterFiles/GetFile/
  ListTopics/caps + varint round-trip + key rebuild + cardinality-mismatch +
  override/mask) keeps the default suite hermetic; crosslang_test.go extended to
  drive the auryn reader end-to-end incl chunk_count.

Verified: go test ./... (hermetic) + -race + crosslang green; Python 103; real
bucket — the reader served a real 171-topic file with ListTopics count-sum (630026)
matching message_count exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auryn catalog migration Milestone 3, server side (the C++ client facet UI is
deferred — task #7). Resolves the migration plan's D3. Reviewed by Claude
(adversarial multi-lens) + Codex; both GO-WITH-FIXES, all findings fixed.

- proto: GetVocabulary RPC (ClientMessage.get_vocabulary=17 /
  ServerMessage.get_vocabulary=19) + the strict customer->site->robot tree
  (DimCustomer/DimSite/DimRobot nested), flat DimSource (V7), TagFacet/
  TagFacetValue; FileFilter gains optional uint64 customer_id=5/site_id=6/
  robot_id=7/source_id=8 (proto3 optional = presence, V5). Additive only —
  existing tags/fields untouched. Go bindings regenerated with the EXACT existing
  protoc 3.21.12 (clean-regen verified byte-identical before the edits).
- catalog.GetVocabulary (vocabulary.go): builds the tree + flat sources + tag
  facets from the auryn dimension tables. Each dimension SELECT is EXISTS-gated
  against files so ORPHAN lookup rows (the builder leaves them on delete/rename by
  design) never surface as ghost nodes (Claude review). TagFacetCap=50 drops
  high-cardinality keys (V4). Per-node file_count via GROUP BY. Empty (non-nil) on
  a legacy store — the live path has no dimensions.
- Dimension predicates: FilterArgs gains Customer/Site/Robot/SourceID *uint64;
  aurynFilterFiles ANDs the present ones. The legacy FilterFiles ignores them.
- WS: CatalogHandler.GetVocabulary maps catalog.Vocabulary -> proto; ListFiles
  maps the FileFilter dim ids; server.go dispatch + handleGetVocabulary (a failure
  is ERROR_INTERNAL — the RPC takes no client input, Claude review).
- Tests: catalog (tree, TagFacetCap exact 50/51 boundary [Codex], dimension
  predicates, orphan pruning, legacy-empty) + ws (proto tree mapping, legacy) +
  crosslang extended (Python DB -> GetVocabulary tree). docs/catalog-vocabulary-rpc.md
  marked server-implemented; §6 indexing/has_error/orphan claims corrected.

Live path unaffected; default suite hermetic; proto additive/backward-compatible.
go test ./... + -race + crosslang green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auryn catalog migration Milestone 4 (plan §3). Streaming-path optimization,
Go-only, no cross-language contract change. Reviewed by Claude (adversarial
multi-lens) + Codex; both GO-WITH-FIXES, all findings fixed.

- 3.3 LRU eviction (internal/format/indexcache.go): ChunkIndexCache now uses a
  container/list LRU (front=MRU, evict the back over cap) replacing the crude
  full-reset — which threw away the ENTIRE cache the instant it hit the cap
  (catastrophic right after a warm-up). Get copies the FileChunkIndex value out
  UNDER the lock (re-Put-safe), reFileID deep-copies Chunks; shared channel/schema
  maps stay read-only. + indexcache_test.go (eviction, re-put recency, restamp).
- 3.1 singleflight (internal/ws/handlers_session.go): cachedChunkIndex DoChan-
  dedupes concurrent cold loads of the same (key,etag) — a stitched multi-file
  open + the warmer + parallel sessions now pay ONE WAN read, not N. The leader
  runs detached (WithoutCancel) so a bailing caller doesn't fail the waiters, but
  BOUNDED by chunkIndexLoadTimeout=2m (Codex+Claude MF-1: WithoutCancel drops the
  deadline; the storage layer has no HTTP timeout, so a hung WAN read would strand
  the leader — and every later opener — forever). nil-cache short-circuits to a
  single direct load (Claude). + singleflight_test.go (dedup==1 call + per-caller
  restamp, leader-error propagation + retry, waiter-bails-leader-completes).
- 3.2 A+ warmer (internal/warm/ + internal/catalog/warm.go): read-only background
  Warmer pre-fills the cache from catalog.WarmEntries (auryn rebuilds key from
  dims, legacy s3_key) with bounded concurrency, skip-if-cached, never-abort on a
  poison file; counts pj_cloud_chunkindex_{warmed,warm_skipped,warm_errors}_total.
  Wired in main.go as a background goroutine AFTER the indexer warm-start (so the
  live path finds the cache hot and skips; it is the cache's warm source once the
  Go indexer is gone post-cutover). + warm_test.go.

Verified: go test ./... + -race (ws/format/warm) + crosslang green; server boots
and logs the warm summary after the indexer warm-start. Live path unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auryn catalog migration M5 §4.5 (Go side). Surfaces the builder's quarantined
files on the operator dashboard so it is visible WHICH files could not be
cataloged and WHY — distinct from a cataloged file's has_error health flag.

- catalog.RecentFailures (failures.go): newest-first quarantine rows, branching on
  the store mode — catalog_failures (auryn read-only) vs indexer_failures (legacy
  Go). Both carry (key, when_ns, error_text); the dashboard's nsHuman render is
  unit-consistent.
- dashboard/server.go: recentFailures routes through catalog.RecentFailures (no
  behavior change on the live legacy path).
- Tests: failures_test.go (both branches, newest-first, limit); the test schema
  helper gained catalog_failures.
- mcap_catalog: bump to the M5 Python builder commit (GCS backend, retry_with,
  synchronous=NORMAL, has_error, unsummarized quarantine + the H1/Defect fixes).

Reviewed by Claude + Codex (GO-WITH-FIXES, all findings fixed). go test ./... +
crosslang green; Python 126.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Go reader surfaces the Python builder's build_metadata so monitoring sees
catalog staleness once the writer is out-of-process. Additive; the live serving
path is unaffected (GetBuildInfo returns not-present on the legacy store). Reviewed
by Codex (GO-WITH-FIXES, both findings fixed).

- const SchemaVersion=3 (lockstep with the Python SCHEMA_VERSION).
- catalog.GetBuildInfo (buildinfo.go): reads build_metadata; Present=false (not an
  error) on the legacy store or before the first build; real query errors propagate.
- metrics: pj_cloud_catalog_{build_id,last_build_timestamp_seconds,files_scanned,
  files_failed} gauges + SetCatalogFreshness. main.go: a 30s background updater
  mirrors build_metadata onto them (no-op/0 on the legacy path), shutdown-safe.
- dashboard: a "Catalog build (Python builder)" panel on the indexer page.
- Tests: GetBuildInfo (auryn present / no-row-yet / legacy-not-present /
  query-error-propagates) + crosslang now asserts the cross-language build_metadata
  round-trip (Python record_build -> Go GetBuildInfo). docs/CATALOG_CONTRACT.md -> v3.

go test ./... + -race + crosslang green; Python 127.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auryn cutover's functional core, behind a DEFAULT-OFF flag so the live path is
byte-unchanged. When catalog.external_builder is set (-external-builder flag /
PJ_CLOUD_EXTERNAL_BUILDER env), the server:
  - opens the catalog READ-ONLY (catalog.OpenReadOnly) — the Python mcap_catalog
    builder is the sole writer;
  - does NOT start the Go indexer (the loop is still constructed for the dashboard's
    "never run" status; the A+ warmer + the §6.5 freshness updater are the
    read-only equivalents).
Default false keeps the legacy in-process indexer + read-write catalog.

VERIFIED END-TO-END: a Python builder built a v3 catalog from local Hive fixtures;
the server booted with -external-builder (indexer DISABLED, store opened read-only)
and served ListFiles over the real WS protocol via devprobe — 6 files with the
rebuilt s3_keys + message_counts. The entire reader (FilterFiles/GetFile/
GetVocabulary/RecentFailures/GetBuildInfo) activates via the s.readOnly branch, so
the cutover is now a config change, not a code change.

Remaining for the full cutover (needs the C++ `make smoke` harness to verify):
§2.6 delete the now-dormant Go indexer code, §5.3 migrate smoke/run.sh to build the
catalog via the Python builder, §6.2a atomic-publish + reader reopen-on-swap, D2
tag-edit write path, §6.1 two-process deploy artifacts.

go test ./... + crosslang green; config round-trips external_builder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ate never ran)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In external-builder mode (catalog.OpenReadOnly) UpdateTags always fails
with catalog.ErrReadOnly, but Hello still advertised the capability as
true — clients would offer an Edit Tags UI guaranteed to fail at runtime.

- catalog: exported Store.ReadOnly() accessor.
- ws Hello: TagEditSupported = !store.ReadOnly() (ResumeSupported unchanged).
- ws handleUpdateTags: fast-path rejection on a read-only store with a
  single operator-facing message (shared const), BEFORE the handler runs —
  an empty-mutation UpdateTags (no set/unset) performs zero writes and
  would otherwise return success (Codex review finding). The errors.Is
  ErrReadOnly branch stays as backstop for future write paths.
- tests: Hello caps pinned for writable (true) + read-only (false) stores;
  UpdateTags wire error pinned on exact code+message for both mutating and
  empty-mutation requests; Store.ReadOnly() covered for both constructors;
  shared newWSTestServer helper extracted in caps_test.go.

Reviewed by Claude (planner) + Codex (adversarial); both findings fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contract §9 'Publish & reopen protocol' added to docs/CATALOG_CONTRACT.md
(byte-identical to the submodule copy): in-place WAL mutation for existing
compatible DBs; create/rebuild via temp + checkpoint-gate + atomic rename;
the READER swap trigger is (dev,inode) file identity — build_id demoted to
a freshness/confirmation counter (monotonic only when the old DB was
readable). Reader-side ReopenIfSwapped is the next task (documented as
not-yet-landed in §9).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pinned reads

The Go read-only store now detects the Python builder's atomic-publish
rebuild (os.replace = new inode at the served path) and reopens without a
restart. Contract per CATALOG_CONTRACT.md §9: the swap trigger is (dev,
inode) file identity — build_id is freshness/confirmation only.

- openVerified/openVerifiedOnce (readonly.go): C1 pool pinning
  (SetMaxOpenConns(1) etc. + forced ping — an unpinned database/sql pool
  would lazily open the replaced file UNVERIFIED on a fresh connection) and
  C2 identity capture (stat -> open+ping+verify -> stat-compare, bounded
  errIdentityRace retry) so the recorded identity provably belongs to the
  opened handle.
- Store.db -> atomic.Pointer[sql.DB] behind DB(); legacy writable path
  unchanged (never swaps). Store.ReopenIfSwapped (reopen.go): fail-closed
  ((false,err) keeps the old generation serving; next tick retries).
- Generation-pinned reads (adversarial-review BLOCKING fix): every
  multi-query logical read pins db := s.DB() once and threads it
  (aurynFilterFiles+tag attach, aurynGetFile, aurynListTopicsForFile,
  legacy branches, GetVocabulary); new compound catalog.GetFileDetail
  replaces the WS GetFile handler's three separate Store calls — a swap
  can no longer mix generations within one response (ids renumber across
  rebuilds, so mixing served WRONG data, not stale data).
- main.go 30s freshness tick drives ReopenIfSwapped; reopen counted
  independently of the freshness read. New metrics:
  pj_cloud_catalog_reopens_total / reopen_failures_total.
- Tests: pool-pinning regression (hold-the-only-conn, proves no second
  physical connection), happy swap, fail-closed + recovery, no-op paths,
  close-under-in-flight-reader contract, and a -race generation-hammering
  test with self-identifying fixtures (customer dim + gen tag + topic name
  must agree per response) — revert-verified to fail without the pinning.

Reviewed by Claude (planner) + Codex (adversarial, 2 blocking + 3
should-fix all fixed). Suite + -race green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (submodule d536934)

Contract updates (byte-identical to the submodule copy): §9 gains the
tags_override rebuild carry-forward + corrupt-served-DB handling; new §10
'Tag-edit IPC' (endpoint shape, key-based addressing, deadline semantics,
bounded surface, socket trust boundary). Go forwarder is the next task.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In external-builder (read-only) mode with catalog.tag_ipc_socket
configured, UpdateTags now forwards to the Python builder instead of
rejecting: resolve file_id -> object key on one pinned generation, POST
set/unset verbatim to the unix-socket endpoint (contract §10), then build
the response by re-resolving key -> CURRENT file_id -> tags_effective —
never the original rowid (ids renumber across rebuilds). Before the
re-read the handler runs ReopenIfSwapped so a rebuild landing mid-request
is picked up immediately (adversarial-review blocking fix), and any
re-read failure falls back to the IPC response's own tags_effective — an
applied edit can never yield a false-negative error. IPC 404 after a
successful phase-1 resolve reports 'no longer present in the catalog',
not 'file not found'.

- internal/tagipc: unix-socket HTTP client (8s timeout, ErrNotFound/
  ErrBusy mapping, response bodies capped + truncation = protocol error).
- internal/catalog: parseHiveKey (invariant rebuild(parse(k)) ==
  TrimLeft(k,"/"), round-trip-pinned vs rebuildHiveKey + the Python
  keyparse), ObjectKeyForFile, EffectiveTagsByKey (lookup-only by
  dimension names, one pinned handle).
- Capability: tag_edit_supported = !ReadOnly() || IPC configured
  (liveness checked at use; proto comment updated, comment-only,
  bindings byte-identical).
- Config catalog.tag_ipc_socket (+ -tag-ipc-socket / PJ_CLOUD_TAG_IPC_SOCKET);
  metrics pj_cloud_tag_ipc_{forwards,failures}_total; remote-addr logging
  (the socket bypasses WS bearer auth — Go is the auth boundary).
- Tests incl. a mid-request generation-swap case (fake IPC server renames
  the DB before responding; an id-based or non-reopening re-read fails it
  — red-first verified) and a key-gone-falls-back-to-IPC-tags companion.

JOINT E2E VERIFIED on this machine: real Python worker+TagEditServer over
a built Hive catalog + this server (-external-builder -tag-ipc-socket) +
the C++ dexory-cloud-cli: tag_edit_supported=true advertised; set/unset
round-trips land in tags_override (Python-written) and come back via the
Go re-read; builder-down yields 'tag edit service unavailable; try again'.

Reviewed by Claude (planner) + Codex (1 blocking + 4 should-fix + 2 nits,
all fixed). Full suite + -race green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
facontidavide and others added 16 commits July 6, 2026 17:32
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n shape

make smoke now exercises the CUTOVER pipeline end-to-end, fully
self-contained from a fresh checkout (no real-corpus dependency):

  synthetic Hive corpus (gen-ci-fixtures -hive + NEW -hive-big 3000-msg
  fixture for multi-batch resume + gen-3d-fixture; 8 files)
  -> dedicated smoke-hive Minio bucket (seed now walks dirs recursively so
     Hive keys are preserved, auto-creates the bucket on the UPLOAD path
     only — -check stays probe-only)
  -> Python builder daemon (--source s3 --no-watch --tag-socket, venv
     ~/.venvs/pj-catalog, AWS_ENDPOINT_URL) is the SOLE catalog writer
  -> Go server -external-builder -tag-ipc-socket (read-only + forwarding).

Expected counts are SELF-DERIVED at runtime (mcaptopics on the local
originals — mcaptopics gained start_ns/end_ns for an independent
time-range oracle); the only hardcoded pins left are the C++ live gtests,
re-pinned to the deterministic synthetic corpus (target ci_synth_big:
3000 msgs/3 topics; hierarchy flag now true). Steps: builder cold build
(build_metadata/catalog_failures asserted) -> discovery vs oracle ->
hermetic+live ctest -> CLI spot checks -> 7 mcapdiff round-trip legs ->
server-restart persistence via (dev,inode) identity -> tag flow that
SURVIVES a --once --rebuild mid-run (inode swap + ReopenIfSwapped +
tags_override carry-forward, live §6.2a proof).

Final-line contract hardened (verdict is always the last line, verified
on both pass and forced-fail paths); builder-death fail-fast; rebuild
timeout; matrix.sh header marks it legacy-real-corpus pending migration.

Reviewed by Claude (planner; caught a hermetic-vs-live grep false alarm
by re-running) + Codex (1 blocking + 6 should-fix + 2 nits, all fixed).
SMOKE PASS verified end-to-end on this machine (live ctest legs RUN:
resume 2.18s multi-batch drop/reconnect).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…thon builder

The integration jobs no longer exercise the Go in-process indexer: fixtures
are seeded under Hive keys, the mcap_catalog builder runs --once as a real
subprocess (PJ_CI_BUILDER_PYTHON), the Go stack opens the catalog via
OpenReadOnly, and the same table-driven per-backend assertions run over it —
plus a warm-rerun leg proving etag-skip structurally (files-table snapshot
byte-identical across the rerun) and catalog_failures emptiness. New
blocking builder-tests job runs the mcap_catalog pytest suite (171 tests).
Both {s3,gcs} legs stay blocking; the GCS emulator env is scheme-qualified
(the Python SDK requires the scheme; the Go SDK tolerates it).

Sabotage/quarantine red-team factored into scripts/sabotage-check.sh and
run BOTH locally (ci-integration.sh) and as a blocking Actions step
(distinct customer=sabotage/ prefix): a corrupt fixture must land in
catalog_failures and never be served — injected-bug-verified non-vacuous.

Activation prerequisites made explicit: the private cross-org submodule
(AurynRobotics/mcap_server, SSH URL) needs a read-only deploy key wired as
the MCAP_CATALOG_DEPLOY_KEY secret (documented at both checkout sites);
an anti-vacuous step asserts TestCIIntegration_Backend PASSed (a lost env
line can never go green via skip); pip deps pinned (bump deliberately).

genmcap.HiveKeyFor exported as the single source of the fixture key shape.
Local driver verified end-to-end on this machine: both legs PASS +
sabotage OK; actionlint clean.

Reviewed by Claude (planner; found the deploy-key blocker independently) +
Codex (same blocker + 3 should-fix + 2 nits, all fixed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Python mcap_catalog builder is now the ONLY catalog writer; the Go
server is a pure read-only reader + streamer. Deleted: internal/indexer/
(whole package), catalog.Open + the writer goroutine + every write API
(UpsertFile, ReplaceTopicsForFile, tag writes) + the embedded legacy
schema + the now-unreachable legacy read branches (OpenReadOnly is the
sole constructor, auryn schema the only shape). ws UpdateTags is IPC-only
(TagEditSupported = tag IPC configured; rejection message names the
actionable config key). Indexer metrics removed; the dashboard indexer
page is repurposed to catalog freshness + quarantine (URL kept), now with
content-asserted tests.

-external-builder / PJ_CLOUD_EXTERNAL_BUILDER / catalog.external_builder
and -poll-interval are accepted-but-ignored no-ops with deprecation
warnings; smoke.sh drops the flag. matrix.sh FAIL-FASTS with a clear
pending-migration message (it depended on the deleted indexer).
NOTE: run.sh and server/deploy/ do not yet start the builder — a fresh
bring-up needs the builder run manually until the immediately-following
§6.1/§6.4 commit restores one-command bring-up.

Tests migrated wholesale from writer-seeded catalogs to raw-SQL auryn
fixtures (shared ws/auryn_fixture_test.go helper: hiveKeyAdapter +
buildAurynCatalog with builder-parity (topic_id,schema_id) fingerprints
and production-shaped empty tags_embedded).

Reviewed by Claude (planner) + Codex (3 blocking — matrix guard here,
run.sh/deploy deferred one commit — + 8 should-fix + 4 nits, all fixed).
Gates: full suite, -race, SMOKE PASS, CI-INTEGRATION PASS (both legs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The final cutover milestone: one-command bring-up and the deploy artifacts
now run the production shape — Python builder daemon (sole catalog writer +
tag-edit IPC, --no-watch rescan mode) FIRST, then the read-only Go server.

run.sh: every target starts builder-then-server; waits for build_metadata
(accepting 'ok' OR 'partial' — a quarantined-files build is published and
servable BY DESIGN); never deletes the catalog DB (tags_override is user
data); reuse branch verifies OUR pidfile, not just the port (live-caught:
a Guacamole container on :8080 previously satisfied the port probe and
run.sh printed 'Backend ready' with no backend — now a clear foreign-
listener error), and repairs a dead builder under a live server; seed
-check rc discriminated; teardown waits for real pid death. make
server-stop reaps both processes with escalation messages.

deploy/: new Dockerfile.builder (pinned deps, submodule-init note) +
pj-cloud-builder.service; compose gains the builder sharing the local-only
catalog volume, a python3 healthcheck gating the server on service_healthy;
both systemd units get StartLimitIntervalSec=0 (the default 5-starts/10s
rate limit killed the documented crash-loop-until-first-publish in ~8s);
false 'checkpoints the read-only handle' claim fixed (mode=ro cannot);
socket trust boundary + failure-path caller logging (ws/server.go);
root .dockerignore; docs pinned-bootstrap parity.

Reviewed by 3 parallel agents (code-reviewer, silent-failure-hunter,
comment-analyzer): 3 critical + 2 high + 4 medium + 11 low findings, all
fixed. Verified live: two-process bring-up + devprobe capabilities + CLI
tag round-trip through the IPC + reuse path + both-process teardown;
compose config validates; SMOKE PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 'Current focus' -> 'Current state': the auryn migration (M1-M6 incl. the
  full tail) is COMPLETE; remaining follow-ups listed (matrix migration,
  CI activation via MCAP_CATALOG_DEPLOY_KEY, facet UI, GCE smoke).
- Harness: smoke's new self-contained two-process shape (synthetic Hive
  corpus, self-derived oracle, live rebuild/reopen leg, venv prereq with
  pinned bootstrap); ci-integration.sh documented; matrix marked
  fail-fast/pending-migration.
- run.sh block: builder-first two-process bring-up, both-process teardown,
  pidfile-based reuse (foreign-listener hard error; this machine's
  Guacamole squats :8080).
- Big-picture architecture: Python builder (sole writer, atomic publish,
  tag IPC) + read-only Go server (OpenReadOnly-only, reopen-on-swap,
  generation-pinned reads, IPC forwarder); 'one static binary' is gone;
  CATALOG_CONTRACT.md flagged as the live contract (both copies).
- Slice changelog left untouched (explicitly historical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…distilled pins

CLAUDE.md is now present-tense instructions + a 'Decisions & pins (do not
re-litigate)' section (21 distilled one-liners with their why: toolbox
shape, zero-decoder delegation, parser_config_json pin, ixwebsocket, LZ4
frames, GCS Generation identity, (dev,inode) swap trigger + key addressing
+ generation-pinned reads, MaxOpenConns(1), EAV-not-JSONB, tags_override
is user data, socket auth boundary, unsummarized-file rejection, protoc
3.21.12 byte-identical regen, ASCII .ui, synthetic-corpus ground truth,
fresh-connection-per-download, RESUME_NOT_POSSIBLE verbatim, wiring order,
server-authoritative overlap, credential_store). The Slice 1-16 changelog
+ historical vendoring narrative moved VERBATIM to docs/history.md
(provenance header; entries describe the state at their time).

Reviewer finding fixed: the 2026-06-22 layout correction's PAYLOAD is
current fact — the live doc now carries a CURRENT REPO LAYOUT banner (PJ4/
plugins are sibling checkouts, not submodules; SDK 0.11.0; only
mcap_catalog/ is a submodule) and the stale 0.8.1/submodule run
instructions are corrected in place, instead of the truth living only in
history.

47KB -> 29KB loaded per session; nothing dropped (byte-for-byte move
verification + content-preservation review).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-M6, catalog file rowids RENUMBER on every external-builder rebuild
and the server hot-swaps the DB mid-connection (ReopenIfSwapped) — a wire
file_id is a generation-scoped handle, so an id the client resolved from
an earlier ListFiles can silently name a DIFFERENT file minutes later
(wrong-file tag edit / wrong topics view). The stable identity is
FileSummary.s3_key, which the client already holds. Design approved by
Codex (key-addressing over a generation-fence: the fence could only fail
stale requests; the key lets them succeed).

- proto: `optional string s3_key` on GetFileRequest(=2)/UpdateTagsRequest(=4).
  Reviewed semantics pinned in comments: key PRESENT => server resolves by
  key and IGNORES file_id (no mismatch validation — surviving a stale id is
  the point; debug log only); present-but-EMPTY => ERROR_INVALID_REQUEST
  "s3_key must be non-empty" (checked BEFORE the tag-IPC capability gate —
  malformation beats capability, Codex review); absent => legacy id path,
  byte-for-byte unchanged. GetFile unknown key => ERROR_NOT_FOUND
  `no file with s3_key "<key>"`; UpdateTags unknown key surfaces via the
  existing IPC-404 'no longer present in the catalog' message.
- catalog: FileIDForKey extracted from EffectiveTagsByKey (lookup-only
  Hive-parse resolve, never creates rows); GetFileDetailByKey pins ONE
  db handle for resolve + three-phase read (generation-consistent).
- ws: GetFile key branch; handleUpdateTagsForwarded uses the client's key
  VERBATIM (no phase-1 id->key resolve to go stale).
- Bindings regen (protoc 3.21.12 via make proto). NOTE: also folds in a
  pre-existing ONE-LINE comment drift — 474030e changed the
  tag_edit_supported proto comment claiming "bindings byte-identical" but
  never regenerated; verified comment-only by a clean pre-edit regen.
- Tests: rebuild id/key-mismatch pair (gen swap renumbers K's id; key-
  addressed GetFile/UpdateTags hit K while id-addressed hits the impostor
  — red-verified by disabling the key path), empty-key both RPCs + the
  no-IPC ordering pin, unknown key, key-over-id precedence, FileIDForKey.
  contract: files.id stability one-liner corrected in BOTH copies
  (submodule bump 387580a -> 3bc610b) — §7 always said full rebuilds
  renumber; the schema block claimed the opposite.

Adversarially reviewed (Claude planner + Codex, 1 should-fix on this half
— the validation-order pin — fixed). go test ./... green; make smoke
PASS (step h tag flow exercises the key path live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… s3_key RPCs

The client half of surviving post-M6 catalog rebuilds (rowids renumber;
the server hot-swaps the DB live), plus the client half of the D2
capability contract. Three changes, each closing a real wrong-data or
guaranteed-failure path found in the post-M6 plugin audit:

- SessionCache re-keyed on sequence NAMES (= FileSummary.s3_key, stable)
  instead of browse-resolved file_ids: after a rebuild a new file could
  inherit a cached file's numeric id and a HIT would replay the WRONG
  file's data with zero transport (realistic: Dexory corpora share a
  uniform topic set). Store path no longer skips caching when the browse
  index went stale mid-download; wasm smoke harness synced. Regression
  test NameKeyedIdentitySurvivesCatalogRebuildRenumbering.
- HelloResponse.capabilities parsed at connect() (was silently dropped):
  ServerCaps{resume_supported, tag_edit_supported} + serverCapabilities()
  accessor; updateTags() fails fast (no frame sent) when PRESENT-and-
  false; absent stays "proceed" so a server verdict lands verbatim. The
  dialog latches it (event-drained, before connectFinished), gates
  buttonEditTags on it AND on !connecting, and clears the latch when a
  new Connect starts (stale-window fixes, Codex review); connect() resets
  both caps before parsing each Hello (reused-connection fix, Codex
  review). CLI: `hello` prints the flags; `tag` fails fast with a clear
  message. Live tag test now SKIPs (not fails) against a no-IPC server.
- GetFile/UpdateTags sent key-addressed: always set s3_key (the sequence
  name verbatim), keep file_id opportunistically (server ignores it when
  the key is present), and an unresolved name proceeds key-only instead
  of failing locally — the tag editor and topics view no longer depend
  on the possibly-minutes-stale name->id index at all. CLI `tag` drops
  its now-redundant pre-list round trip.
- tests/server_caps_test.cpp (new, hermetic): minimal in-process
  ix::WebSocketServer faking the Hello/ListFiles/GetFile/UpdateTags
  wire — capability parse both ways, the exact fail-fast message with
  zero frames sent, s3_key presence on both RPCs, and key-only success
  for a never-listed name.

Adversarially reviewed (Claude planner + Codex; 2 should-fix on this half
— the capability stale-latch pair — fixed). Build -Werror clean; ctest
hermetic 34/34; make smoke PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…abf)

Brings in the ~10x faster S3 full_reconcile (parallel summary reads +
listing-stat skip + coalesced GETs). Validated end-to-end: 175 builder unit
tests, `make smoke` PASS, a real Dexory-staging backfill (24,437 files in
~19 min vs ~5.5h), and two Codex reviews (verdict: MERGE).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A cached FileChunkIndex is dominated by its per-topic SchemaData (ROS2 message
definitions), so entries vary ~100x in size. The entry-count cap (4096) let a
real 24k-file Dexory catalog pin ~10 GB RSS, and the warmer fetched ALL 24k
files' indexes on startup (~36 min of WAN reads) only to evict all but 4096.

- ChunkIndexCache is now byte-bounded (NewChunkIndexCacheSized; LRU evicts by
  estimated bytes, always keeping the MRU). Entry-count mode kept for tests.
  ApproxBytes() estimates footprint (SchemaData-dominated).
- The warmer stops after warming ~one cache-worth of NEW bytes (Warmer.Budget),
  measured cumulatively — the cache self-evicts to stay under the cap, so a
  "current size >= cap" check never trips; cumulative warmed bytes does.
- catalog.chunk_index_cache_bytes config knob (default 512 MiB); main wires it
  to both the cache cap and the warmer budget.

Validated on the real Dexory staging catalog (24,437 files): RSS 10 GB -> ~700 MB
stable, warm 36 min -> ~50 s, serving unchanged. Full server suite + new tests
(byte eviction, oversized-entry, warmer budget stop / warm-all) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gister

Eager per-topic ensureParserBinding at bindSession registered EVERY session
topic in the host data tree — and real Dexory rosbox files declare hundreds of
summary channels that never published (mcap doctor: 366 summary/data channel
mismatches on a sample), so an All-topics fetch filled the tree with
drag-to-nothing entries.

The binding now happens on a topic's FIRST message, inside the same host-write
critical section as pushMessage (threading discipline unchanged). No data ->
no binding -> no tree entry. The load-bearing details moved verbatim: schema
fields forwarded as-is, and the non-empty "{}" parser_config_json that keeps
parser_ros's specialized handlers (tf/pointclouds/images) selected.

Consequences wired through:
- bindSession only resolves schemas now; parser refusals surface mid-stream,
  so fetch_worker snapshots per-topic outcomes AFTER the download.
- The driver test pins the new contract: no bindings after bindSession, the
  first message binds exactly once with verbatim fields, a refusal flips the
  topic undecodable with the host's reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One coordinated pass over the browser panel, driven by hands-on testing
against the full 24,437-file Dexory staging catalog:

Topics panel
- All|Custom selection mode: radio pair (host-rendered DualOptionsWidget) in
  the header. All = every listed topic downloads, table inert; Custom = the
  user's rows. Mode persisted (dexory_cloud/topics_all).
- Selection is 100% user-owned: the /tf + /tf_static guarantee moved from
  force-selected rows to IMPLICIT fetch-time inclusion — per-event selection
  mutation fought the host's selection applier (every click/drag got a
  programmatic clear+reselect a tick later).
- Zero-count rows disabled (visible in the browser, not selectable); the
  second column is "Count" (per-topic message counts — the wire carries no
  per-topic byte size); header shows "Topics (sel/total)".

Sequences panel
- Aggregate OFF by default; toggling it clears the selection (row identities
  change between the file and session views).
- No selection restore at launch (old persisted keys actively cleared), and
  the hidden-selection policy: filters that hide EVERY selected row drop the
  selection instead of keeping invisible state (topics + slider reset).
- ElideMiddle on the Name column (right elision hid the distinguishing
  filename tail); 6px margins on both sides of the table/filter splitter.
- The D8 "Folder:" prefix combo is REMOVED (ui row, handler, state, helper
  header, test): with Hive keys the top-level prefix IS the customer
  dimension, which the Basic metadata filter covers properly.

Metadata filter
- The Basic|Advanced QTabWidget became a bold "Metadata based filter" label +
  radio-pair DualOptionsWidget with two swapped panes (the tab chrome fought
  the app styling); active mode + Basic selections persisted, with persisted
  values pruned against fresh server data (stale constraint would silently
  hide every row while its combo showed "(any)").

Connection & downloads
- Connection-state visibility: stateful Connect tooltip, invalid-tint on the
  URI field while disconnected, "Sequences - not connected/connecting" header
  (auto-connect is silent by design; its outcome no longer is).
- Download button gains a first-unmet-requirement tooltip; fetch status says
  "MiB decoded" (decompressed payloads exceed zstd on-disk sizes ~8x).
- PERF: heavy seqTable keys (rows/headers/visible set) are delivered only when
  the view cache actually rebuilt — the unconditional per-tick push serialized
  megabytes of JSON per frame at 24k rows and saturated the GUI thread.

Chrome
- Connect/refresh/cert buttons 28x28/24px icons (the app convention, matching
  comboUri's QSS-pinned 28px outer); regex toggles 28x28; panel margins; the
  Info toggle hidden (plumbing kept: one line restores it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bulky catalog RPC responses (ListFiles, GetVocabulary, GetFile, OpenSession,
UpdateTags) went out as raw protobuf while session batches were already ZSTD
internally. Add an ADDITIVE, per-client-negotiated compressed-envelope path for
those responses. Measured 5.4x on a real 200-file dexory ListFiles page.

Wire (proto/pj_cloud.proto):
- Hello.accepted_response_encodings (repeated CompressionEncoding) — the opt-in.
- EncodedServerMessage{encoding, uncompressed_size, body} in the ServerMessage
  oneof (field 20). An enum, not hardcoded-zstd, so a future codec needs no new
  wrapper. Outer request_id/subscription_id stay ZERO — the inner message is the
  sole routing authority. proto3 defaults give full bidirectional back-compat;
  no protocol_version bump. Go bindings regenerated with protoc 3.21.12.

Server:
- conn.SendPriority wraps a response iff the client negotiated it, the payload is
  allowlisted (HelloResponse/Error/Progress/Eos/MessageBatch always raw), it
  clears the threshold, AND compression saves bytes (else raw fallback). One
  shared stateless klauspost zstd encoder (EncodeAll is concurrency-safe),
  bounded concurrency. Negotiated flag captured after auth, under atomic.
- Dedicated transport-level config (server.response_compression) — RPC
  compression applies to catalog-only connections, so it does NOT reuse the
  session body_zstd_level. Prometheus input/output byte counters.

Client (plugin + CLI share the transport):
- Hello advertises ZSTD via a shared fillHello() builder (initial + reconnect
  can't drift). onBinaryFrame unwraps BEFORE routing with a hardened decoder
  (session_decode::decodeEncodedEnvelope): 64 MiB ceiling, exactly one zstd
  frame with no trailing data, known content size == announced size, exact-size
  alloc + decompress, nested-wrapper rejected. Any deviation drops the frame.

Visibility:
- Session batches are already ZSTD inside MessageBatch, so track wire bytes
  received and surface "X MiB received / Y MiB decoded" in the fetch status,
  making the compression ratio visible end-to-end.

Tests: 11 Go wrap-matrix tests (+race), 5 C++ envelope-decoder tests (round-trip,
size-mismatch, out-of-bounds, trailing-data, garbage). make smoke PASS — the
round-trip legs now exercise the compressed envelope live.

Design cross-checked with Codex before implementation (decoder DoS-hardening and
the pagination reality-check were its load-bearing catches).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vel 1

Deploy: add a Dexory-specific EC2 runbook (Docker Compose shape) and its paired
artifacts — real-S3 compose (no Minio, creds via the EC2 instance IAM role, IMDS
hop-limit noted) and its server config. Wire them into server/deploy/README.md.

- docs/ec2-deploy.md — step-by-step: IAM role, IMDS hop-limit=2, security group,
  bring-up, verify, TLS, day-2 ops. Explicitly Dexory-scoped (Asensus is the
  separate GCS/GCE deploy).
- server/deploy/docker-compose.dexory.yml, config.dexory-ec2.yaml — builder +
  server against the real dexory bucket; catalog volume stays a LOCAL named
  volume (never EFS).

Compression: default server.response_compression.level 3 -> 1. Catalog RPC bodies
(Hive keys, tags) compress heavily even at the fastest level — measured 5.2x at
level 1 vs 5.4x at level 3 on a real ListFiles page, so the higher level buys
almost nothing for extra CPU.

Docs: bring the current-state docs in line with the code (no history touched).
- config.example.yaml gains the server.response_compression block (the
  authoritative config surface was missing it).
- CLAUDE.md: architecture note + a Decisions/pins entry for the compressed
  envelope (allowlist, zero outer IDs, hardened decoder, level 1, deflate stays
  off) + docs/ec2-deploy.md in the Documents list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ownload status

Sequences column:
- Add an aligned caption grid: "Filter by time range:" (now leads) over
  "Filter by filename:", sharing one left edge (column-0 labels).
- Move the metadata filter out of the former table/query vertical splitter to
  a FIXED-height row (140px) directly under the filters; the sequence table now
  takes all remaining height. Rename its header to "Filter by metadata:" and
  drop the bold so all three captions match.

Download status:
- Give the fetch status its OWN full-width row (fetchStatusLabel, above the
  topic table) instead of overwriting the Topics header, so long phase/transition
  strings no longer collide with the All|Custom toggle. Visible only during a
  download; wordWrap so nothing clips.
- Show a compact "Fetching NN% - X.X MiB/s": the percentage is wire bytes
  received vs the server pre-flight estimate (an upper bound, clamped < 100),
  with a topic-count fallback until the estimate + first bytes arrive. The
  received/decoded figures + compression ratio move to the row's hover tooltip
  (attached via the host's setFieldValid tooltip path).
- Wire the numeric pre-flight estimate to the dialog through a new optional
  pullEstimate worker callback (the phase line already carried it as prose).

All 33 plugin ctests pass (incl. plugin-load + .ui well-formed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@facontidavide
facontidavide merged commit 259a309 into main Jul 13, 2026
3 of 7 checks passed
facontidavide added a commit that referenced this pull request Jul 17, 2026
… path

Two long-standing CI failures (present since PR #1, unrelated to any code
change):

integration (s3/gcs) — the minio + fake-gcs emulators were declared as GitHub
`services:` containers, but a services block CANNOT override the image command,
and `minio/minio`'s default entrypoint does NOT start a server. So `mc ready
local` never turned healthy and BOTH matrix legs died at "Initialize
containers" before any test ran. Replaced the services block with explicit
`docker run -d --network host` steps carrying the real `server /data` /
fake-gcs command — mirroring infra/{minio,fake-gcs}/docker-compose.yml (which
work) and matching the `--network host` the seed steps already use — plus a
30s HTTP health poll (/minio/health/live, /storage/v1/b). Verified locally:
both emulators start and pass the exact probes in ~2s (the old `mc ready local`
never did).

wasm-compile — the job did `cd PJ4/pj-official-plugins/toolbox_dexory_cloud/
wasm`, a path from before the plugin was relocated to plugin/toolbox_dexory_
cloud/. The cd failed (exit 1) before build.sh ran. Repointed the build step
and the artifact upload paths to plugin/toolbox_dexory_cloud/wasm. build.sh is
BASH_SOURCE-relative and its protobuf-dependent TUs are gated/skipped without a
native build, so the pure decode-core smoke stands alone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants