Skip to content

Atlas step 4: serve mode — job protocol, worker pool, in-memory temp store #5354

Closed

Description

@dezren39

Step 4 of the Atlas binary+service epic (#5350). Design of record: doc/2026-11-04T08-07-13Z_design_atlas_binary_service/ (§2 protocol, §3 clients/auth, §6 non-goals).

Scope

  • atlas-proto crate: the job protocol's request/response types, shared by the server and the CLI's via-service client so they cannot drift.
  • serve subcommand: HTTP+JSON on 127.0.0.1 — POST /v1/search (one-shot), POST /v1/search/jobs (202 + job_id + per-job capability token), GET /v1/search/jobs/{id} (status), GET /v1/search/jobs/{id}/results?cursor=&limit= (pages of the immutable fused ranking), DELETE /v1/search/jobs/{id}, GET /v1/search/jobs/{id}/chunk/{chunk_id} (ceiling-checked), GET /health (no auth, no data).
  • Worker pool: FTS and vector branches of a job execute concurrently; fusion runs once over complete branch pools; fetch-to-fill escalation happens inside the vector branch only.
  • Temp store: in-memory, sharded by job_id, TTL 10 min from completion / 60 min hard cap from submit, sweeper-enforced. Nothing persists across restart; in-flight jobs fail expired on restart and clients resubmit. (Rationale — reproducible + snippet-bearing + perishable — is design §2; do not re-litigate it into a database without an epic-level decision.)
  • Auth + ceiling enforcement: bearer tokens from ~/.local/state/atlas-search/service.json (0600), per-client ceiling_cap; effective ceiling = min(requested, cap), recorded on the job and reported in the envelope; the init verb generates the client table (Drew's CLI + the §3 v1 rows). A get-chunk outside the job ceiling returns the missing-chunk shape.
  • Backpressure (design §2, config-tunable): ≤4 concurrent branch executions globally, ≤2 concurrent + 8 queued jobs per client, pool ≤400 at submit, index-fill escalation ≤3200, saturation → 429 + Retry-After + queue depth; queued-too-long jobs fail expired (120 s).

Dependencies: step 2 (core); step 1 via step 3's embedder wiring. Serves the Turso backend only in v1 (local backend stays CLI-side).

Verification gate (exit criteria)

  • Protocol conformance tests: submit→page a full ranking and confirm page-concatenation == the one-shot result == the standalone CLI's result for the same query/config.
  • Ceiling tests: a personal-capped token cannot obtain local-only content via search, paging, another job's token, or get-chunk — each attempted path demonstrated closed.
  • Restart test: in-flight jobs fail expired, no result data survives on disk anywhere under the state dir.
  • Saturation test: the 5th concurrent branch execution queues/429s per the configured behavior.

Out of scope: packaging/deploy (step 5), issuing real client tokens for other machines (step 6 cutover), Aster (not a v1 client — design §3/§6).

Acceptance criteria

  • atlas-proto crate defines the job protocol types shared by server and CLI via-service client; serve exposes the full §2 surface (one-shot search, job submit/status/results paging, chunk fetch, delete, unauthenticated /health) on 127.0.0.1.
  • Protocol conformance: page-concatenation of a job's results == the one-shot result == the standalone CLI result for the same query/config.
  • Ceiling enforcement proven by tests: a personal-capped token cannot obtain local-only content via search, paging, another job's token, or get-chunk.
  • Temp store is in-memory only: restart fails in-flight jobs as expired and no result data survives on disk under the state dir; TTLs (10 min post-completion / 60 min hard cap) are sweeper-enforced.
  • Backpressure behaves per design §2: the 5th concurrent branch execution queues/429s with Retry-After + queue depth.

Activity

  1. added
    source/humanFiled directly by a human
    priority/normalOrdinary priority (the default)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Nov 4, 2026
  2. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Intake review — fleet monitor, 2026-11-04.

    Related: epic #5350; depends on step 2 (core) and step 1 via step 3's embedder wiring; packaging/deploy is step 5 (#5355), token issuance is step 6 (#5356).

    Notes: the in-memory temp store is a decided design point (§2 rationale recorded in the issue) — the acceptance criteria pin the restart/TTL behavior so it is tested, not relitigated. The ceiling tests are the security substance of this step: four distinct exfiltration paths each demonstrated closed.

    Risks: protocol drift between server and CLI client is the main structural risk; the shared atlas-proto crate is the mitigation and is in scope.

    Suggested approach: proto crate + serve skeleton first, conformance test (three-way equality) early, then ceiling/restart/saturation tests as the exit gate.

  3. added a commit that references this issue on Nov 4, 2026
  4. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    Step 4 complete — PR #5392, merged 5acabc82

    What was built

    • atlas-proto (new crate): the job protocol's serde types, shared by the server and the CLI's via-service client — SearchRequest (full CLI search surface), JobCreated, JobStatusResponse (incl. queue depth/position), SearchPage, JobDeleted, JobSummary/JobListResponse (recovery), HealthResponse, uniform ErrorResponse. No policy lives in proto.
    • Serve mode (crates/atlas-search/src/server.rs, std-only HTTP on TcpListener, loopback enforced): POST /v1/search, POST /v1/search/jobs, GET /v1/search/jobs (recovery list), GET /v1/search/jobs/{id}, GET .../results?cursor=&limit=, DELETE .../{id}, GET .../chunk/{chunk_id}, GET /health (no auth). One shared embedder (the CLI's lazy embedder behind a load-state handle: cold → warming → ready, reported by /health); Turso backend resolved through the existing read-only token chain — the service holds no write credential and builds only SELECTs.
    • Branch execution: atlas-core gained a BranchPoolExecutor seam (run_search_with_executor); standalone run_search uses the default backend executor with byte-identical semantics. Serve mode's executor runs hybrid branches concurrently on scoped threads and fuses once; single-branch jobs execute inline — per Drew's usage profile, the primary real-world path (laptop-local, unfiltered, vector mode on IndexFill/IndexFixed) is a first-class one-slot, one-branch execution with no FTS work and no extra thread hop. Index backends are fully wired through proto, CLI, and server (live-verified below). Core defaults unchanged (the flip stays Atlas step 6: golden-parity gate (Gate B) + cutover #5356's).
    • init verb: generates the v1 client table into ~/.local/state/atlas-search/service.json (0600, enforced at load): drew-cli cap secret-bearing; openchamber, fork-server, sandbox-scripts cap personal. Token values go to the file only. Serve config also carries listen address, vec_table, and tunable limits.
    • Auth + clamp: every non-health request carries a client bearer token; requested ceilings clamp to the token cap and the effective ceiling is what branch filters are built from, what the job records, and what envelopes report. Job-scoped reads accept the owning client's token or the job's per-job capability token only (another job's token: 401; another client: 403). Job-scoped get-chunk returns the missing-chunk shape for chunks above the job ceiling (including unknown sensitivity — fail-closed).

    Design delta 1 — async queue (Drew, 2026-11-04)

    Submit returns immediately with a job handle in queued (or running). A bounded strict-FIFO execution queue sits in front of the ≤4 concurrent branch executions: per-client caps of 2 running computations + 8 queued, plus a global queue cap of 8; overflow is 429 + Retry-After with the live queue depth in the body. Accepted queued jobs never expire in the queue — the earlier 120 s queue-TTL idea is dropped in favor of bounded admission — and the stored ranking's 10-minute TTL runs from completion (the 60-min hard cap remains only as the pre-completion outer bound). Jobs carry queue_depth/queue_position. The CLI client polls status, so its visible behavior stays blocking; queueing is a protocol property for agent clients.

    Design delta 2 — per-client job recovery (Drew, 2026-11-04)

    GET /v1/search/jobs with a client bearer token lists that client's own jobs — job id, state, query summary (query/mode/pool), effective ceiling, submit/expiry times, queue position — metadata only, never results, never capability tokens. Job tokens cannot list (401). Pickup after a disconnect is job id + capability token on the existing job endpoints.

    Design delta 3 — dedupe cache (Drew, 2026-11-04)

    Execution is shared through a result cache whose unit is a Computation; every submission still gets its own job id + capability-token handle over the shared immutable ranking (listing/pickup/per-job authorization unchanged).

    • Key = the effective request after clamping: (query, mode, k, pool, group_by_session, expand, with_message, model, embedder, trunc, vector_backend, kind[], topic[], project[], client[], since, until, effective_ceiling). Exact-key sharing only: the effective ceiling is in the key, so a lower-ceiling request can never be served from a higher-ceiling entry, and filters are in the key, so no stored ranking is ever subsetted to fake a filtered one.
    • In-flight coalescing: an identical submit while the computation is queued/running attaches to it (observing its queued/running state) — no second branch execution, no queue slot consumed. Completed hit: an identical submit within the entry's TTL returns an already-done handle.
    • TTL runs from the shared computation's completion. Failed computations are evicted immediately so a later identical submit retries; existing handles keep the failure. Deleting a handle releases only that handle, never the shared computation.

    Verification

    • cargo test --workspace: 43 passed, 0 failed (1 ignored = Gate A ceremony test, unchanged from step 1). cargo clippy --workspace --all-targets: 0 warnings. cargo fmt --check: clean. Toolchain: rustup 1.99.0, RUSTC_WRAPPER= empty.
    • Protocol tests (stub backend with branch-call counting): paging concatenation == one-shot with zero re-execution across pages; capability/ceiling enforced on search, paging, cross-job tokens, and get-chunk; TTL-from-completion proven with a job queued longer than the TTL (250 ms TTL, 400 ms queue wait) — readable at completion, 410 after; bounded FIFO proven with execution order + overflow 429/Retry-After: 7/queue-depth; vector index modes proven single-branch with the right BranchQuery shapes; recovery list proven (owner sees, other client doesn't, job token can't list, fresh-connection pickup); health/delete.
    • Dedupe gate tests: (a) two identical unfiltered submits → branch calls stay at 2, results byte-identical, distinct job ids/tokens; (b) same query at personal vs secret-bearing effective ceilings → two executions, canary never crosses; (c) kind-filtered vs differently-filtered vs unfiltered → three executions, exact filtered repeat coalesces; (d) coalescing during running and queued states, shared results after release.
    • Live E2E (worktree server, scratch config on 127.0.0.1:18799, production Turso read-only, real Snowflake embedder): 15/15 — hybrid/scan parity vs standalone CLI on 4 questions (ids+scores+rank_sources identical); index-fixed and index-fill unfiltered vector parity, 10 results each; dedupe live (first submit 6 s, identical second 0.00 s, status done); recovery list + pickup live; 3 parallel submits → queued/running/running, all complete; /health reports the shared embedder warm. Fallback gate: server killed → CLI prints the loud stderr warning, envelope carries the via-service: …; served standalone note, results identical to standalone. Restart gate: pre-kill job id → 404 job_expired_or_unknown after restart; the server wrote zero result-data files anywhere (state is memory-only; the scratch dir held only the config the test created).

    Notes for step 5 (#5355)

    • Config/token provisioning: atlas-search init already produces service.json (0600) with the v1 table; each client copies only its own token into ~/.config/atlas-search/client.json ({"name", "token"}, 0600) — the CLI resolves tokens via ATLAS_SEARCH_TOKEN → ATLAS_SEARCH_CLIENT_CONFIG → that path. Real cross-machine issuance is step 6's, per the epic.
    • Loader paths (carried from step 3, reconfirmed live): the packaged binary must resolve ONNX Runtime from its nix closure (ATLAS_ORT_LIB today) with libstdc++.so.6 + libz.so.1 reachable — the E2E ran with exactly those two store paths on LD_LIBRARY_PATH. Model dir resolution order: ATLAS_MODEL_DIR → ~/.local/state/atlas-search/model/<rev> → HF cache snapshot (Q1 state-dir fetch lands with packaging).
    • Port 18760 (from the design) is the serve default in the generated config; the E2E used 18799 to avoid colliding with anything live — step 5 still owes the port-inventory check.
    • serve takes --job-ttl-ms / --hard-ttl-ms overrides for operators/tests; limits otherwise come from service.json.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort/lLarge: multi-day or open-ended; expect staged check-inskind/enhancementmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/humanFiled directly by a humanstatus/needs-triageNot yet triaged; needs a first look

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions