Skip to content

feat(host-store): antseed file state + call-ledger route identity off the filesystem#38

Merged
jmlago merged 5 commits into
mainfrom
host-store-peer-offers
Jun 29, 2026
Merged

feat(host-store): antseed file state + call-ledger route identity off the filesystem#38
jmlago merged 5 commits into
mainfrom
host-store-peer-offers

Conversation

@jmlago

@jmlago jmlago commented Jun 29, 2026

Copy link
Copy Markdown
Member

What this does (and why)

The host's operational state — the antseed marketplace book, the buyer's
wallet/escrow status, and the per-call ledger — lived in JSON/JSONL files
on a shared volume and in in-process dicts. That state is per-pod and can't be
shared between the router and the ingress (they run as separate containers), nor
survive a restart. #36 began moving it into a single Postgres store both
containers reach; this PR continues that migration.

Three atomic commits. 1 and 2 move the antseed market and status files
into the store (after which sources/antseed.py no longer touches disk at all,
and the shared market volume is deleted). 3 enriches the call ledger with the
identity of the route that actually served each call — the raw fact the next
step (#4) will derive per-route stats from. Commit 3 depends on engine #23
(chosen.served_by), already merged and pinned here.


Commit 1 — peer_offers (antseed market book)

One RAW row per (peer, service) — the seller's announced prices/cap/reputation
as columns. The antseed sidecar is the sole writer (antseed network browse),
_load_market the sole reader. The 15-min sliding window that merge-market.js
unioned by hand becomes a read-time filter on observed_at; the sidecar prunes
past the window.

Commit 2 — buyer_status (pin + escrow + wallet)

One row per buyer pid — raw buyer-reported fields. Written by write-status.js
(poll loop) and control.js (after a wallet op); read by _pinned_peer +
balances. With both off the filesystem, sources/antseed.py no longer touches
disk and the antseed-market volume (+ both mounts) is removed.

Commit 3 — calls.served_by + calls.tokens_cached

The fact table gains the executed route identity (served_by, from the
engine's chosen — the marketplace peer that served the call, or the provider
itself for a direct route) and the cache-token breakdown. These are the raw
per-call facts #4 derives per-route stats from. Threaded engine chosen
x_router → ingress → calls. Bumps the core submodule to engine #23 (merged).
route_key is left unchanged — deriving a peer-granular route key from
served_by is #4's job; this commit only captures the raw fact. ttft omitted
(nothing measures it yet → an idle column).

Invariants

  • Store raw, derive by query. Columns hold raw seller/buyer/engine values; no
    scoring host-side. served_by is captured raw; combining it into a route key
    is deferred to test: BDD user-flow suite (behave) + AntSeed dev-wallet setup #4.
  • In-place migration. calls evolves via idempotent ALTER TABLE ... ADD COLUMN IF NOT EXISTS — the store's first table evolution (CREATE TABLE IF NOT
    EXISTS never alters an existing table).
  • Fail-soft throughout; behaviour preserved (offers_sync / market_book /
    _pinned_peer / balances / OpenAI-compat unchanged but for their source).
  • Irreversible: new DB state (peer_offers, buyer_status, two calls
    columns); market.json / status-<id>.json retired.

Dependency sovereignty (Axis 4)

pg@8.16.3 only in the sidecar (Node), pinned; the buyer_status UPSERT shared in
antseed/store.js so the two writers can't drift. No new Python dep (psycopg
from #36). The sidecar→Postgres edge is the same shape as router→PG / ingress→PG:
all three depend on the shared store, not on each other.

Verification

  • pytest full suite: 411 passed, 2 skipped, 0 failed (against the compose Postgres).
  • Engine fix(core): bump engine to ee31e81 — preserve marketplace offer prices #23 unit suite: 568 passed, 0 failed.
  • Real antseed sidecar (rebuilt image, live DHT + funded wallet ~1.97 USDC):
    populates peer_offers (5 live peers) + buyer_status (the real wallet) in
    Postgres; /x/market surfaces them; the router routes off them.
  • served_by + tokens_cached: the ALTER migration applies in place on boot; a
    live chat records both in calls end to end (against engine fix(core): bump engine to ee31e81 — preserve marketplace offer prices #23).
  • behave e2e — incl. the money/antseed spend path and the headless-chromium UI:
    54 scenarios passed. The remaining failures are pre-existing environment
    limits of this dev stack, NOT this change: codex isn't configured as a
    subscription provider here, so the codex-seeded flows fail (05_providers
    api_kind, 09_flow1 no_candidates, the 08_dashboard_ui row that needs the
    seeded activity); and one antseed peer returned insufficient_deposits. The
    antseed scenario that failed still routed via peer_offers — i.e. this
    change worked; the peer rejected the payment.

Remaining migration (#36's plan)

  1. route_* views derived from calls (keyed by served_by) · 5.
    _stats/usage-history → queries over calls · 6. cleanup (drop legacy JSON +
    migrate_legacy_json).

Move the antseed marketplace book from market.json (a file on a shared
volume, unioned by hand in merge-market.js) into the Postgres host store —
the next slice of the JSON/in-process migration after #36.

Form delta:
- Definition: a new `peer_offers` table holds one RAW row per (peer, service)
  — the seller's announced prices/cap/reputation as columns, not interpreted.
  The antseed sidecar is the sole writer (it runs `antseed network browse`);
  sources/antseed._load_market is the sole reader. The 15-min sliding window
  that merge-market.js unioned by hand is now a read-time filter on
  observed_at (WHERE observed_at >= now - window); the sidecar prunes rows
  past the window.
- Invariants: store raw, derive by query — no scoring host-side; the negative
  / cached>input / reputation gates stay in offers_sync. Fail-soft: a DB error
  degrades to "no antseed candidates" exactly as a missing dump did.
  Behaviour preserved: offers_sync / market_book unchanged.
- Irreversible: peer_offers is new DB state; market.json is retired.

Changes:
- host_store.py: peer_offers schema (PK (peer_id, service) + observed_at
  index) and a window-filtered peer_offers() reader; truncate hook updated.
- antseed/write-market.js: replaces merge-market.js — flattens the browse
  dump to (peer, service) rows, UPSERTs into peer_offers (type-cleaning at the
  write, mirroring the old Python coercion), prunes past the window.
- sources/antseed.py: _load_market reads host_store.peer_offers(); the file /
  staleness / flatten code and the now-dead coercion helpers are removed.
- Dockerfile.antseed: pin pg@8.16.3 + NODE_PATH so the writer can require it.
- compose.yml: DATABASE_URL + postgres dependency for the antseed service (it
  already shares the llm-router-internal network with postgres).
- tests: seed peer_offers (shared conftest helper) instead of market.json; new
  host_store peer_offers round-trip + window tests.

Sovereignty (Axis 4): pg is the boring standard Postgres client, pinned, and
lives only in the sidecar; no new Python dependency (psycopg is from #36).

Verification: full suite 409 passed, 2 skipped, 0 failed against the compose
Postgres; the real Node writer -> Postgres -> Python reader round-trip,
non-dump validation and window prune checked; the full stack boots healthy and
/x/market surfaces a seeded antseed peer end to end.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jmlago, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb93aac4-119a-4c52-9cba-3e7d96ee4e53

📥 Commits

Reviewing files that changed from the base of the PR and between e261bca and d71c6e7.

📒 Files selected for processing (2)
  • antseed/entrypoint.sh
  • tests/test_schema_contract.py
📝 Walkthrough

Walkthrough

The PR moves AntSeed marketplace offers and buyer status from shared JSON files to Postgres-backed tables and APIs. It adds DB writers, rewires the source and compose setup to use host_store, and records extra response metadata in the request ledger.

Changes

AntSeed market and status storage migration

Layer / File(s) Summary
Host store schema and readers
host_store.py
Adds the peer_offers table and index, implements peer_offers(window_ms=900_000), and includes peer_offers in truncate_all_for_tests().
Market writer and runtime wiring
antseed/write-market.js, antseed/entrypoint.sh, Dockerfile.antseed
Adds a stdin-to-peer_offers writer, routes the entrypoint to it, and updates the image to install pg and ship the new script.
Buyer status writer path
antseed/store.js, antseed/control.js, antseed/write-status.js, antseed/entrypoint.sh
Adds shared buyer-status SQL helpers, persists refreshed buyer status in Postgres, and introduces a standalone status writer used by the entrypoint.
Compose and source read path
compose.yml, sources/antseed.py
Removes the shared market volume, adds Postgres connectivity for antseed, and switches AntSeed reads from filesystem files to host_store APIs.
Test helpers and migrated coverage
tests/conftest.py, tests/test_host_store.py, tests/test_antseed_offers.py, tests/test_sources.py
Adds host-store seeding helpers and updates AntSeed/store tests to seed and assert against peer_offers and buyer status in Postgres.

Response metadata recording

Layer / File(s) Summary
Ledger schema and router metadata
shim.py, host_store.py, tests/test_host_store.py
Adds served_by to router metadata, extends the calls schema and insert path with served_by and tokens_cached, and updates call-recording coverage.
Proxy response capture
auth_proxy.py
Parses served_by and tokens_cached from streaming and non-streaming responses before recording the request.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • genlayerlabs/unhardcoded#36: Introduces the shared host_store Postgres persistence layer that this PR extends with peer_offers and buyer-status accessors.

Poem

🐇 I hopped from files to tables bright,
And tucked the market into Postgres light.
My ears caught served_by, crisp and new,
With cached tokens stamped right through.
Fresh rows bloom where old dumps slept,
While this rabbit grins and database-kept.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main migration of antseed state and call-ledger metadata into host storage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch host-store-peer-offers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@antseed/entrypoint.sh`:
- Around line 66-68: The Postgres write path in entrypoint.sh can hang
indefinitely during connect/query, so wrap the node "$LIB/write-market.js"
invocation in a timeout and treat timeout as a soft failure that preserves the
last good window. Update the write step inside the existing refresh loop guard
so stalled pg access exits non-zero without blocking subsequent iterations, and
keep the current failure handling behavior for the write-market.js path.

In `@antseed/write-market.js`:
- Around line 72-83: The refresh logic in write-market.js is committing each
UPSERT and the prune independently, so a failure can leave peer_offers partially
updated. Update the main async flow that uses Client.connect(), the rows loop,
and the DELETE from peer_offers to run inside a single database transaction, and
make sure any error triggers a rollback before the existing stderr/exitCode
handling.

In `@sources/antseed.py`:
- Around line 112-113: The browse freshness check in antseed’s host snapshot
logic is incorrectly deriving staleness from whether peer_offers() returns any
rows, so an empty but successful browse is marked stale. Update the browse flow
around peer_offers() and the _stats["stale"] assignment to use a separate
heartbeat/freshness signal (or freshness metadata returned with the rows)
instead of not rows, and make snapshot_stats() reflect actual data freshness
rather than row count.
- Around line 112-113: The discover path is still vulnerable to a slow Postgres
read because `host_store.peer_offers()` can block while lazily opening the pool
and initializing schema before `antseed` reaches its fail-soft handling. Update
the `peer_offers()` flow in `host_store` to use explicit connection and
statement timeouts so the call fails fast and returns an empty result quickly,
and keep the `antseed` discovery path (`rows = host_store.peer_offers(...)`)
relying on that bounded behavior.

In `@tests/test_antseed_offers.py`:
- Around line 24-28: The autouse _clean fixture currently only relies on
host_store_clean, but it also needs to reset the in-memory route cache state so
tests do not leak global mutations between cases. Update _clean in
tests/test_antseed_offers.py to clear the route_tool_capability cache/state used
by test_offers_sync_drops_supports_tools_for_learned_incapable_route before
yielding, so test_offers_sync_defaults_tool_capability_for_meets_req always
starts from a clean route state regardless of file order.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47519e2b-4ea3-4ca8-8d71-d728286a663c

📥 Commits

Reviewing files that changed from the base of the PR and between 86d0d89 and e13afe0.

📒 Files selected for processing (11)
  • Dockerfile.antseed
  • antseed/entrypoint.sh
  • antseed/merge-market.js
  • antseed/write-market.js
  • compose.yml
  • host_store.py
  • sources/antseed.py
  • tests/conftest.py
  • tests/test_antseed_offers.py
  • tests/test_host_store.py
  • tests/test_sources.py
💤 Files with no reviewable changes (1)
  • antseed/merge-market.js

Comment thread antseed/entrypoint.sh
Comment thread antseed/write-market.js
Comment on lines +72 to +83
(async () => {
const client = new Client({ connectionString: process.env.DATABASE_URL });
try {
await client.connect();
for (const r of rows) await client.query(UPSERT, r);
await client.query("DELETE FROM peer_offers WHERE observed_at < $1",
[now - WINDOW_MS]);
} catch (e) {
process.stderr.write(`write-market: ${e.message}\n`);
process.exitCode = 4; // DB error -> keep window
} finally {
await client.end().catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== antseed/write-market.js ==\n'
wc -l antseed/write-market.js
sed -n '1,140p' antseed/write-market.js

printf '\n== antseed/entrypoint.sh ==\n'
wc -l antseed/entrypoint.sh
sed -n '1,220p' antseed/entrypoint.sh

printf '\n== transaction usage search ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' '\b(BEGIN|COMMIT|ROLLBACK)\b|client\.query\("BEGIN"|client\.query\("COMMIT"|client\.query\("ROLLBACK"' antseed . || true

Repository: genlayerlabs/unhardcoded

Length of output: 8600


Wrap the refresh in a transaction.
write-market.js autocommits each UPSERT and the prune separately. If a later row or the DELETE fails, earlier rows stay committed even though entrypoint.sh treats the non-zero exit as “keep the last good window,” leaving a partially refreshed peer_offers.

🧰 Tools
🪛 ast-grep (0.44.0)

[error] 76-77: Avoid SQL injection
Context: client.query("DELETE FROM peer_offers WHERE observed_at < $1",
[now - WINDOW_MS])
Note: [CWE-89] Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection').

(sql-injection-javascript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/write-market.js` around lines 72 - 83, The refresh logic in
write-market.js is committing each UPSERT and the prune independently, so a
failure can leave peer_offers partially updated. Update the main async flow that
uses Client.connect(), the rows loop, and the DELETE from peer_offers to run
inside a single database transaction, and make sure any error triggers a
rollback before the existing stderr/exitCode handling.

Comment thread sources/antseed.py
Comment on lines +112 to +113
rows = host_store.peer_offers(STALE_AFTER_S * 1000)
self._stats["stale"] = not rows

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Fresh empty browses now look stale.

peer_offers() only returns rows that are still inside the sliding window, and Line 113 turns [] into stale=True. That means a successful browse that finds zero peers/services is indistinguishable from a stale snapshot or a DB read failure, so snapshot_stats()/dashboard freshness regresses even when the data is current. This needs a separate browse heartbeat (or freshness metadata returned alongside the rows) instead of deriving staleness from row presence alone.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sources/antseed.py` around lines 112 - 113, The browse freshness check in
antseed’s host snapshot logic is incorrectly deriving staleness from whether
peer_offers() returns any rows, so an empty but successful browse is marked
stale. Update the browse flow around peer_offers() and the _stats["stale"]
assignment to use a separate heartbeat/freshness signal (or freshness metadata
returned with the rows) instead of not rows, and make snapshot_stats() reflect
actual data freshness rather than row count.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files:\n'
git ls-files | rg '(^|/)(antseed\.py|.*host_store.*|.*store.*|.*peer.*)$' || true

printf '\nSearch for symbols:\n'
rg -n "def (_get_pool|peer_offers|offers_sync)|ConnectionPool\(|statement_timeout|connect_timeout|timeout" sources . || true

Repository: genlayerlabs/unhardcoded

Length of output: 15890


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'host_store.py relevant sections:\n'
sed -n '120,190p;380,460p' host_store.py | cat -n

printf '\nantseed.py relevant section:\n'
sed -n '90,150p' sources/antseed.py | cat -n

printf '\nTests around host_store timeouts/behavior:\n'
sed -n '1,220p' tests/test_host_store.py | cat -n

Repository: genlayerlabs/unhardcoded

Length of output: 22850


Bound the Postgres read on the discover path. sources/antseed.py:23 calls host_store.peer_offers(), and host_store.py lazily opens the pool and runs schema init without connect/statement timeouts. A slow or unreachable Postgres can block discovery before the fail-soft except runs; add explicit DB timeouts so this degrades to [] quickly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sources/antseed.py` around lines 112 - 113, The discover path is still
vulnerable to a slow Postgres read because `host_store.peer_offers()` can block
while lazily opening the pool and initializing schema before `antseed` reaches
its fail-soft handling. Update the `peer_offers()` flow in `host_store` to use
explicit connection and statement timeouts so the call fails fast and returns an
empty result quickly, and keep the `antseed` discovery path (`rows =
host_store.peer_offers(...)`) relying on that bounded behavior.

Comment on lines +24 to +28
@pytest.fixture(autouse=True)
def _clean(host_store_clean):
# The market book now lives in the host store (peer_offers); every test seeds
# it and needs the per-test truncation (skips if Postgres is unavailable).
yield

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reset the in-memory route caches in the autouse fixture.

This fixture now only truncates Postgres, but test_offers_sync_drops_supports_tools_for_learned_incapable_route mutates global route_tool_capability state for antseed|qwen3-235b-a22b|peerA, while test_offers_sync_defaults_tool_capability_for_meets_req expects that same route to start clean. That makes the file order-dependent.

Suggested fix
 import route_reliability as rr  # noqa: E402
 import route_latency as rl  # noqa: E402
+import route_tool_capability as tc  # noqa: E402
 from sources.antseed import AntSeedSource  # noqa: E402
 from conftest import seed_peer_offers as _seed_market  # noqa: E402
 
 
 `@pytest.fixture`(autouse=True)
 def _clean(host_store_clean):
     # The market book now lives in the host store (peer_offers); every test seeds
     # it and needs the per-test truncation (skips if Postgres is unavailable).
+    rr.reset()
+    rl.reset()
+    tc.reset()
     yield
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture(autouse=True)
def _clean(host_store_clean):
# The market book now lives in the host store (peer_offers); every test seeds
# it and needs the per-test truncation (skips if Postgres is unavailable).
yield
`@pytest.fixture`(autouse=True)
def _clean(host_store_clean):
# The market book now lives in the host store (peer_offers); every test seeds
# it and needs the per-test truncation (skips if Postgres is unavailable).
rr.reset()
rl.reset()
tc.reset()
yield
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_antseed_offers.py` around lines 24 - 28, The autouse _clean
fixture currently only relies on host_store_clean, but it also needs to reset
the in-memory route cache state so tests do not leak global mutations between
cases. Update _clean in tests/test_antseed_offers.py to clear the
route_tool_capability cache/state used by
test_offers_sync_drops_supports_tools_for_learned_incapable_route before
yielding, so test_offers_sync_defaults_tool_capability_for_meets_req always
starts from a clean route state regardless of file order.

Twin of the peer_offers move: the antseed buyer's status (session pin +
escrow + wallet) goes from status-<id>.json on the shared volume to the
Postgres host store. With both off the filesystem, sources/antseed.py no
longer touches disk and the antseed-market volume is removed entirely.

Form delta:
- Definition: a new `buyer_status` table holds one row per buyer pid — the
  raw buyer-reported fields (pinned_peer_id, deposits_available/_reserved,
  wallet_address, connection_state) as columns. The antseed sidecar writes it
  (write-status.js on the poll loop + control.js after a wallet op);
  sources/antseed reads it (_pinned_peer + balances).
- Invariants: store raw — deposits stay the strings the buyer reports and are
  coerced on read, exactly as the JSON status was. Fail-soft: a missing row /
  store error degrades to "no pin, no balance" as a missing status file did.
  Behaviour preserved: _pinned_peer / balances unchanged but for the source.
- Irreversible: buyer_status is new DB state; status-<id>.json is retired and
  the antseed-market volume (+ both mounts) is dropped.

Changes:
- host_store.py: buyer_status schema + a buyer_status(pid) reader; truncate
  hook updated.
- antseed/store.js: shared buyer_status row shape + UPSERT, used by both
  writers so they can't drift.
- antseed/write-status.js: replaces the inline node -e + atomic_write; reads
  `buyer status --json`, UPSERTs buyer_status, validates (non-status -> no
  write).
- antseed/control.js: refreshStatus UPSERTs buyer_status via a pg pool instead
  of writing the file; still returns the fresh status for the HTTP response.
- antseed/entrypoint.sh: write_status calls write-status.js; the now-dead
  atomic_write helper is removed; comments updated.
- sources/antseed.py: _pinned_peer + balances read host_store.buyer_status;
  the file / json / Path / market_dir machinery is removed (no disk access).
- Dockerfile.antseed: COPY store.js + write-status.js.
- compose.yml: drop the antseed-market volume and its router/antseed mounts.
- tests: seed buyer_status (shared conftest helper) instead of status files;
  new host_store buyer_status round-trip/absent test.

Verification: full suite 410 passed, 2 skipped, 0 failed against the compose
Postgres; the real write-status.js -> Postgres -> Python reader round-trip and
non-status validation checked; all four sidecar JS files pass node --check;
the full stack boots healthy and creates buyer_status on boot.
@jmlago jmlago changed the title feat(host-store): peer_offers — antseed market book off the filesystem feat(host-store): antseed market + buyer status off the filesystem Jun 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_sources.py (1)

493-497: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Don't hide schema/setup regressions behind pytest.skip().

This skips on any exception, not just an unavailable Postgres. If the new peer_offers/buyer_status setup is broken (schema init, TRUNCATE, SQL shape, etc.), the AntSeed source tests will be reported as skipped instead of failing, which can mask the migration break this helper is meant to catch. Limit the skip to connectivity/bootstrap failures and let other exceptions fail the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sources.py` around lines 493 - 497, The test helper in host_store
reset/truncate handling is catching every Exception and turning all failures
into pytest.skip(), which can hide schema or SQL regressions. Update the
try/except around host_store.reset() and host_store.truncate_all_for_tests() so
only expected Postgres connectivity/bootstrap failures are skipped, while other
exceptions are re-raised and fail the AntSeed source tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@antseed/control.js`:
- Around line 55-61: The refreshStatus() flow currently parses stdout from
run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS) without checking the
returned exit code, so non-zero CLI failures can still be treated as valid
status data. Update refreshStatus() to inspect the run() result code before JSON
parsing/upsert, and immediately return a failed/null status when the CLI exits
non-zero; keep the existing JSON parse and pool.query/UPSERT_BUYER_STATUS path
only for successful runs.
- Line 26: The shared Postgres pool created in control.js currently has no
timeout, so pool.query() calls in refreshStatus() can hang indefinitely when the
database is wedged. Update the Pool initialization for the shared pool to
include an appropriate timeout setting, and make sure the existing
refreshStatus(), /deposit, /withdraw, and /status flows use that same pool
instance so requests fail fast instead of waiting forever.

In `@antseed/entrypoint.sh`:
- Line 74: The status writer call in the entrypoint can still hang during pg
connect/query because the current fallback only ignores exit failures. Update
the `node "$LIB/write-status.js"` invocation in `entrypoint.sh` to run under a
timeout, or add an explicit database timeout inside `write-status.js` so the
`writeStatus` flow cannot block indefinitely.

In `@antseed/store.js`:
- Around line 21-24: The UPSERT row builder in buyerStatusRow currently accepts
any object and turns missing fields into nulls, which can overwrite a valid
buyer_status record. Add a shared shape validator for status objects and use it
before calling buyerStatusRow and before any connect/upsert path so only
properly shaped status data is accepted. Keep the check centralized near
buyerStatusRow and update callers to reject invalid CLI error objects before row
creation.

In `@antseed/write-status.js`:
- Around line 16-22: The current object-only guard in write-status.js lets
invalid status objects like {} or {"error": ...} proceed into the UPSERT path
and open Postgres unnecessarily. Reuse the shared status validator from store.js
in the write-status entrypoint before creating the Client, and exit early for
any invalid shape so only a valid status reaches buyerStatusRow and
UPSERT_BUYER_STATUS.

In `@tests/conftest.py`:
- Around line 81-100: `seed_buyer_status` is overwriting previously seeded buyer
fields with NULL when callers omit them. Update the UPSERT in this helper so
omitted values preserve the existing `buyer_status` columns instead of replacing
`pinned_peer_id` and `connection_state`, or change the helper contract to
require a complete row and update the `_antseed_source()`/balance test call
sites accordingly. Use the `seed_buyer_status` helper and its `ON CONFLICT (pid)
DO UPDATE` clause as the fix point.

---

Outside diff comments:
In `@tests/test_sources.py`:
- Around line 493-497: The test helper in host_store reset/truncate handling is
catching every Exception and turning all failures into pytest.skip(), which can
hide schema or SQL regressions. Update the try/except around host_store.reset()
and host_store.truncate_all_for_tests() so only expected Postgres
connectivity/bootstrap failures are skipped, while other exceptions are
re-raised and fail the AntSeed source tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49ba1da5-8470-4d8a-91d0-9d22473b47ba

📥 Commits

Reviewing files that changed from the base of the PR and between e13afe0 and ca3b567.

📒 Files selected for processing (12)
  • Dockerfile.antseed
  • antseed/control.js
  • antseed/entrypoint.sh
  • antseed/store.js
  • antseed/write-status.js
  • compose.yml
  • host_store.py
  • sources/antseed.py
  • tests/conftest.py
  • tests/test_antseed_offers.py
  • tests/test_host_store.py
  • tests/test_sources.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • Dockerfile.antseed
  • tests/test_antseed_offers.py

Comment thread antseed/control.js

// One pool for the long-lived control server (write-status.js, the poll-loop
// twin, is one-shot and uses a plain Client instead).
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- antseed/control.js ---'
nl -ba antseed/control.js | sed -n '1,240p'

echo
echo '--- searches ---'
rg -n "statement_timeout|query_timeout|connectionTimeoutMillis|Pool\\(|DATABASE_URL|refreshStatus|/deposit|/withdraw|/status" antseed . -g '!node_modules' -g '!dist' -g '!build'

Repository: genlayerlabs/unhardcoded

Length of output: 229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect package metadata to identify pg version and any timeout-related docs/config.
echo '--- package files mentioning pg ---'
rg -n '"pg"|node-postgres|postgres' package.json antseed package.json . -g '!node_modules' -g '!dist' -g '!build'

echo
echo '--- antseed/control.js nearby context (if file is large) ---'
wc -l antseed/control.js

Repository: genlayerlabs/unhardcoded

Length of output: 1648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- antseed/control.js ---'
nl -ba antseed/control.js | sed -n '1,240p'

echo
echo '--- timeout-related search ---'
rg -n "statement_timeout|query_timeout|connectionTimeoutMillis|Pool\\(|DATABASE_URL|refreshStatus|/deposit|/withdraw|/status" . -g '!node_modules' -g '!dist' -g '!build'

echo
echo '--- package metadata ---'
if [ -f package.json ]; then
  sed -n '1,220p' package.json
fi
if [ -f antseed/package.json ]; then
  sed -n '1,220p' antseed/package.json
fi

Repository: genlayerlabs/unhardcoded

Length of output: 229


🌐 Web query:

node-postgres Pool connectionTimeoutMillis query_timeout statement_timeout documentation

💡 Result:

In node-postgres, the Pool configuration supports several timeout parameters that govern different stages of the database interaction lifecycle [1][2]. connectionTimeoutMillis This parameter controls the maximum time, in milliseconds, that the client will wait to establish a connection to the database [3][4]. If a connection cannot be acquired from the pool within this time, it returns an error [3][4]. The default is no timeout [1][2]. query_timeout This is a client-side timeout managed by the node-postgres library [4][5]. It specifies the maximum time, in milliseconds, that the library will wait for a response from the database after a query has been sent [1][4][5]. If the time is exceeded, the driver stops listening for a response, but the database may continue executing the query on the server side [5]. The default is no timeout [1][2]. statement_timeout This parameter is passed directly to the PostgreSQL server [5]. It defines the maximum time, in milliseconds, that an individual SQL statement is allowed to run on the server [1][4]. If the limit is reached, PostgreSQL cancels the statement and returns an error to the client [4][5]. This is generally considered the more robust way to handle long-running queries, as it ensures the server stops processing the request [5]. The default is no timeout [1][2]. Recommended Relationship Best practice often involves using both, setting the query_timeout slightly longer than the statement_timeout [6][5]. This allows the database to gracefully cancel and report the error (via statement_timeout), while providing a client-side safety net (via query_timeout) in the event of network issues or other failures where the database fails to send a response [5]. Configuration These options can be passed directly to the Pool constructor [3][7]: const pool = new Pool({ connectionTimeoutMillis: 3000, statement_timeout: 20000, query_timeout: 25000 }); [8], [3], [1], [4], [7], [2], [5], [9]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- antseed/control.js (first 260 lines with line numbers via awk) ---'
awk '{printf "%4d  %s\n", NR, $0}' antseed/control.js | sed -n '1,260p'

echo
echo '--- pool/timeout usage in antseed ---'
grep -RInE 'Pool\\(|connectionTimeoutMillis|query_timeout|statement_timeout|refreshStatus|/deposit|/withdraw|/status' antseed || true

echo
echo '--- antseed/control.js line count ---'
wc -l antseed/control.js

Repository: genlayerlabs/unhardcoded

Length of output: 6055


Add a timeout to the shared Postgres pool. refreshStatus() awaits pool.query(), so a wedged database can leave successful /deposit, /withdraw, and /status responses hanging indefinitely. antseed/control.js:26

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/control.js` at line 26, The shared Postgres pool created in
control.js currently has no timeout, so pool.query() calls in refreshStatus()
can hang indefinitely when the database is wedged. Update the Pool
initialization for the shared pool to include an appropriate timeout setting,
and make sure the existing refreshStatus(), /deposit, /withdraw, and /status
flows use that same pool instance so requests fail fast instead of waiting
forever.

Comment thread antseed/control.js
Comment on lines 55 to +61
const r = await run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS);
let data;
try { data = JSON.parse(r.stdout); } catch (_) { return null; }
if (data === null || typeof data !== 'object') return null;
data.fetched_at_ms = Date.now();
const tmp = STATUS_FILE + '.' + process.pid + '.tmp';
try {
fs.writeFileSync(tmp, JSON.stringify(data));
fs.renameSync(tmp, STATUS_FILE);
} catch (_) { try { fs.unlinkSync(tmp); } catch (__) {} }
await pool.query(UPSERT_BUYER_STATUS, buyerStatusRow(data, PID));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not trust non-zero status CLI output.

run() returns code, but refreshStatus() ignores it. If the CLI exits non-zero while emitting JSON, this path can still return ok: true and upsert bad status data.

Suggested guard
   const r = await run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS);
+  if (r.code !== 0) return null;
   let data;
   try { data = JSON.parse(r.stdout); } catch (_) { return null; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const r = await run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS);
let data;
try { data = JSON.parse(r.stdout); } catch (_) { return null; }
if (data === null || typeof data !== 'object') return null;
data.fetched_at_ms = Date.now();
const tmp = STATUS_FILE + '.' + process.pid + '.tmp';
try {
fs.writeFileSync(tmp, JSON.stringify(data));
fs.renameSync(tmp, STATUS_FILE);
} catch (_) { try { fs.unlinkSync(tmp); } catch (__) {} }
await pool.query(UPSERT_BUYER_STATUS, buyerStatusRow(data, PID));
const r = await run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS);
if (r.code !== 0) return null;
let data;
try { data = JSON.parse(r.stdout); } catch (_) { return null; }
if (data === null || typeof data !== 'object') return null;
data.fetched_at_ms = Date.now();
try {
await pool.query(UPSERT_BUYER_STATUS, buyerStatusRow(data, PID));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/control.js` around lines 55 - 61, The refreshStatus() flow currently
parses stdout from run(['buyer', 'status', '--json'], STATUS_TIMEOUT_MS) without
checking the returned exit code, so non-zero CLI failures can still be treated
as valid status data. Update refreshStatus() to inspect the run() result code
before JSON parsing/upsert, and immediately return a failed/null status when the
CLI exits non-zero; keep the existing JSON parse and
pool.query/UPSERT_BUYER_STATUS path only for successful runs.

Comment thread antseed/entrypoint.sh
< "$raw" | atomic_write "$MARKET_DIR/status-antseed.json" || true
# upsert the buyer status into the host store's buyer_status (validate + DB
# error -> exit non-zero, keep the last good row)
node "$LIB/write-status.js" < "$raw" || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files
git ls-files 'antseed/*' | sed -n '1,200p'

# Map the entrypoint and status writer structure
ast-grep outline antseed/entrypoint.sh --view expanded || true
ast-grep outline antseed/write-status.js --view expanded || true
ast-grep outline antseed/lib/write-status.js --view expanded || true

# Read the relevant script sections with line numbers
for f in antseed/entrypoint.sh antseed/write-status.js antseed/lib/write-status.js; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    wc -l "$f"
    cat -n "$f" | sed -n '1,220p'
  fi
done

# Find PostgreSQL usage and any timeout handling in the antseed directory
rg -n --hidden --no-messages 'timeout|pg\.|new Client|Client\(|pool|connect\(|query\(' antseed

Repository: genlayerlabs/unhardcoded

Length of output: 8136


Bound the status writer’s DB call. node "$LIB/write-status.js" can still block in pg connect/query because write-status.js has no timeout, so || true only masks exited failures. Wrap this invocation in timeout or add a DB timeout in antseed/write-status.js.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/entrypoint.sh` at line 74, The status writer call in the entrypoint
can still hang during pg connect/query because the current fallback only ignores
exit failures. Update the `node "$LIB/write-status.js"` invocation in
`entrypoint.sh` to run under a timeout, or add an explicit database timeout
inside `write-status.js` so the `writeStatus` flow cannot block indefinitely.

Comment thread antseed/store.js
Comment on lines +21 to +24
function buyerStatusRow(d, pid) {
return [pid, str(d.pinnedPeerId), str(d.depositsAvailable),
str(d.depositsReserved), str(d.walletAddress),
str(d.connectionState), Date.now()];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject non-status objects before shaping the UPSERT row.

buyerStatusRow({}) currently produces a row of null status fields, so object-shaped CLI errors can clobber the last good buyer_status row. Centralize a shape check here and have callers use it before connecting/upserting.

Suggested shared validator
 const str = (v) => (v === null || v === undefined) ? null : String(v);
+const STATUS_KEYS = [
+  "pinnedPeerId", "depositsAvailable", "depositsReserved",
+  "walletAddress", "connectionState",
+];
+
+function isBuyerStatus(d) {
+  return d !== null && typeof d === "object" &&
+    STATUS_KEYS.some((k) => Object.prototype.hasOwnProperty.call(d, k));
+}
 
 function buyerStatusRow(d, pid) {
+  if (!isBuyerStatus(d)) throw new TypeError("not an antseed buyer status");
   return [pid, str(d.pinnedPeerId), str(d.depositsAvailable),
           str(d.depositsReserved), str(d.walletAddress),
           str(d.connectionState), Date.now()];
 }
 
-module.exports = { UPSERT_BUYER_STATUS, buyerStatusRow };
+module.exports = { UPSERT_BUYER_STATUS, buyerStatusRow, isBuyerStatus };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function buyerStatusRow(d, pid) {
return [pid, str(d.pinnedPeerId), str(d.depositsAvailable),
str(d.depositsReserved), str(d.walletAddress),
str(d.connectionState), Date.now()];
const str = (v) => (v === null || v === undefined) ? null : String(v);
const STATUS_KEYS = [
"pinnedPeerId", "depositsAvailable", "depositsReserved",
"walletAddress", "connectionState",
];
function isBuyerStatus(d) {
return d !== null && typeof d === "object" &&
STATUS_KEYS.some((k) => Object.prototype.hasOwnProperty.call(d, k));
}
function buyerStatusRow(d, pid) {
if (!isBuyerStatus(d)) throw new TypeError("not an antseed buyer status");
return [pid, str(d.pinnedPeerId), str(d.depositsAvailable),
str(d.depositsReserved), str(d.walletAddress),
str(d.connectionState), Date.now()];
}
module.exports = { UPSERT_BUYER_STATUS, buyerStatusRow, isBuyerStatus };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/store.js` around lines 21 - 24, The UPSERT row builder in
buyerStatusRow currently accepts any object and turns missing fields into nulls,
which can overwrite a valid buyer_status record. Add a shared shape validator
for status objects and use it before calling buyerStatusRow and before any
connect/upsert path so only properly shaped status data is accepted. Keep the
check centralized near buyerStatusRow and update callers to reject invalid CLI
error objects before row creation.

Comment thread antseed/write-status.js
Comment on lines +16 to +22
if (d === null || typeof d !== "object") process.exit(3); // JSON but not a status

(async () => {
const client = new Client({ connectionString: process.env.DATABASE_URL });
try {
await client.connect();
await client.query(UPSERT_BUYER_STATUS, buyerStatusRow(d, PID));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the status shape before opening Postgres.

With the current object-only check, {} or {"error": ...} reaches the UPSERT path. After adding the shared validator in store.js, use it here so invalid status exits before any DB work.

Suggested caller update
-const { UPSERT_BUYER_STATUS, buyerStatusRow } = require("./store.js");
+const { UPSERT_BUYER_STATUS, buyerStatusRow, isBuyerStatus } = require("./store.js");
 ...
-if (d === null || typeof d !== "object") process.exit(3); // JSON but not a status
+if (!isBuyerStatus(d)) process.exit(3);                   // JSON but not a status
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@antseed/write-status.js` around lines 16 - 22, The current object-only guard
in write-status.js lets invalid status objects like {} or {"error": ...} proceed
into the UPSERT path and open Postgres unnecessarily. Reuse the shared status
validator from store.js in the write-status entrypoint before creating the
Client, and exit early for any invalid shape so only a valid status reaches
buyerStatusRow and UPSERT_BUYER_STATUS.

Comment thread tests/conftest.py
Comment on lines +81 to +100
def seed_buyer_status(pid, pinned_peer_id=None, deposits_available=None,
deposits_reserved=None, wallet_address=None,
connection_state=None):
"""Seed the antseed buyer status (pin + escrow + wallet) as the sidecar's
write-status.js / control.js do — one row per buyer pid. Deposits are the raw
buyer-reported strings."""
import time
with host_store._get_pool().connection() as conn:
conn.execute(
"INSERT INTO buyer_status (pid, pinned_peer_id, deposits_available,"
" deposits_reserved, wallet_address, connection_state, fetched_at)"
" VALUES (%s,%s,%s,%s,%s,%s,%s)"
" ON CONFLICT (pid) DO UPDATE SET"
" pinned_peer_id=EXCLUDED.pinned_peer_id,"
" deposits_available=EXCLUDED.deposits_available,"
" deposits_reserved=EXCLUDED.deposits_reserved,"
" wallet_address=EXCLUDED.wallet_address,"
" connection_state=EXCLUDED.connection_state, fetched_at=EXCLUDED.fetched_at",
(pid, pinned_peer_id, deposits_available, deposits_reserved,
wallet_address, connection_state, int(time.time() * 1000)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Preserve existing buyer fields when reseeding a status row.

This helper always UPSERTs the full buyer_status row, so callers that only pass deposits/wallet values overwrite the preseeded pinned_peer_id and connection_state with NULL. _antseed_source() seeds default pins first, and the later balance tests already reuse this helper without pinned_peer_id, so the stored row no longer mirrors the sidecar state these fixtures are supposed to represent. Either preserve existing columns when an argument is omitted, or require callers to pass a complete row explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/conftest.py` around lines 81 - 100, `seed_buyer_status` is overwriting
previously seeded buyer fields with NULL when callers omit them. Update the
UPSERT in this helper so omitted values preserve the existing `buyer_status`
columns instead of replacing `pinned_peer_id` and `connection_state`, or change
the helper contract to require a complete row and update the
`_antseed_source()`/balance test call sites accordingly. Use the
`seed_buyer_status` helper and its `ON CONFLICT (pid) DO UPDATE` clause as the
fix point.

…bump)

#3 of the operational-store migration: enrich the `calls` fact table with the
two raw per-call facts the #4 route/analytics views will derive from — the
executed route identity and the cache-token breakdown. Prerequisite for keying
per-route stats off the ledger.

- Submodule bump core 97d0333 -> 537e204 (unhardcoded-engine #23): the engine's
  `chosen` now carries `served_by` — the marketplace peer that served the call,
  or the provider itself for a direct route (never nil). Host suite green on it.
- host_store.py: `calls` gains `served_by TEXT` + `tokens_cached BIGINT`, applied
  to existing tables via idempotent `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
  (CREATE TABLE IF NOT EXISTS never alters an existing table — the store gains its
  first in-place migration). insert_call maps both. route_key is left unchanged:
  deriving a peer-granular route key from served_by is #4's job; this commit only
  captures the raw fact.
- shim.py: `_build_x_router` surfaces `served_by` from `chosen` (tokens_cached was
  already there).
- auth_proxy.py: the ingress threads served_by + tokens_cached off x_router into
  the recorded call (both stream and unary paths) -> insert_call.

ttft was intentionally NOT added: nothing measures it yet, so the column would be
idle (Axis 3). error_type was already a column.

Verification: full suite 411 passed, 2 skipped, 0 failed against the compose
Postgres; the ALTER migration applies in place on boot; a live chat records
served_by + tokens_cached in `calls` end to end against engine #23.
@jmlago jmlago changed the title feat(host-store): antseed market + buyer status off the filesystem feat(host-store): antseed file state + call-ledger route identity off the filesystem Jun 29, 2026
jmlago added 2 commits June 29, 2026 15:45
…olumn contract

peer_offers and buyer_status are CREATEd by the Python host store but WRITTEN by
the Node antseed sidecar (write-market.js, antseed/store.js) and seeded by Python
test mimics (conftest). Three places must agree on the column set and nothing at
runtime makes them: the readers are fail-soft, so a renamed/added/dropped column
degrades antseed to "no candidates" silently -- and the unit suite can't see it,
because it seeds via the Python mimic, not the real Node writer (green proves the
reader works, not that Node and Python agree).

Add a static contract test that parses the column list out of all three sources
and asserts it matches per table. Pure text parsing: no DB, no node runtime, runs
in the ordinary unit suite; red on any drift (verified by injecting a rename).
The live behave e2e stays the only thing exercising the real Node writer; this
guards the part that drifts.
Prod runs the sidecar as the image now (not the inline node command), so the
entrypoint must keep the inline's safety: a CHANGE_ME / unset-secret placeholder
is not a valid identity and the CLI would reject it. Unset it when it isn't a
64-hex string so the buyer falls back to a generated key on the data volume
(matching the previous inline behaviour); the prod secret is a real hot-wallet.
@jmlago jmlago merged commit 9d576f5 into main Jun 29, 2026
1 check passed
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.

1 participant