v2's recall gains its second channel. infra/embed.py is an
OpenAI-compatible /embeddings client that never raises and caches a
query's vector in-process by model and folded text. application/
find_index.py builds one card matrix per catalog in the background on
the first v2 find: vectors are read from the archive's object store
under find-vectors/<model>/<card sha256>, only missing cards are
embedded, and those are written back. Without a store they live in the
process; without the API the channel stays off and the build is retried
later. The judged event and the searchlog row record the query's
embedding time and error.
The object store gains named objects for these vectors only, each body
carrying its own card hash and size. numpy joins the server extra for the
matrix product. Settings find_embed_api_key (falls back to treg's
OpenRouter key on the OpenRouter URL), find_embed_model, find_embed_url,
find_embed_timeout_s.
The bench builds the vectors before scoring when a key is set, and its
judge cache key now includes the job question's wording.
FastAPI computes one unique_id per route from the first method its set
yields, so the multi-method /call relay published seven operations under
one hash-order-dependent id and warned on every schema build. Build the
schema from per-method views of multi-method routes (and GET-only views
of HEAD-widened routes) without touching the live routing table, drop
the snapshot script's workaround, and fail tests on the warning.
For the Google Sheets add-on. /table/ takes exactly the request /call/ takes and runs the same call
on the same road (gates, key injection, hold and settle, audit row, Idempotency-Key); only the
ending differs. /call/ is unchanged: it passes every query parameter to the provider and must
return the provider's answer as is (non-negotiable 4), so this is a sibling route, not an option.
- routers.call.run_call_surface: the body of call_tool, shared by /call/, /catalog/call/ and
/table/; each passes its own `finish`. /call/ passes _relay_answer (stream unchanged, async
descriptor, review invitation), so its behavior is the same.
- domain.table: a pure, stdlib-only converter with its own import-linter contract. Routed jobs take
their columns from the contract (flat: output fields then served_by, a miss is 0 rows; a required
list: one row per item, people-shaped lists mapped to fixed columns first, the map is data). Hub
tools take the manifest's output fields. Anything else is flat, list or nested (tables + summary),
found from the lists of objects in the answer, with one-item wrappers opened. Never raises: an
answer it cannot shape is raw.
- application.table: reads the answer (8 MiB cap, raw and truncated beyond), asks the provider for
uncompressed bytes, keeps the call's X-Treg-* headers, maps an upstream non-2xx to a JSON error
with the same status. No money moves here: the call is settled before the answer returns.
- Behind TREG_TABLE_ENABLED (+ TREG_TABLE_TEAMS / TREG_TABLE_USERS), off by default. With it off
the route answers a plain 404 that leaves no audit row.
Tests: tests/test_table.py (converter on saved answers; end to end: flag, four shapes, same charge
as /call/, upstream error releases the hold, Idempotency-Key replay costs 0, a hub tool).
Adds docs/context/architecture/table.md; updates composition.md, proxy-model.md, api.md,
import-boundaries.md and the AGENTS.md non-negotiable 4 line.
Brings main's 86 commits (dashboard boot and loading, legacy dashboard removal, test pruning,
overflow and routing fixes) together with the tool hub branch.
Conflicts, both sides kept unless noted:
- the legacy dashboard stays deleted, as on main;
- App.vue and the dashboard state: main's search page and loading states plus the hub pages;
- ci.yml: main's Postgres job, with the hub tests added to its list;
- dev-local.sh: main's server environment plus the hub flag passthrough;
- test_call_application_contract.py, test_marketplace_call.py: main's pruned files plus the
hub branch's sync `settle: usage` test.
Not conflicts: main and the hub branch fixed the same CompanyEnrich empty-page billing; main's
rule runs first, so the hub branch's copy and its test are dropped. The Listing-tab test reads
the Vue source instead of the deleted legacy page.
Brings main's 24 commits since the 2026-09-21 merge: the new Vue dashboard in frontend/ with
the account-based rollout (legacy frozen as src/treg/web/dashboard-legacy/), Olostep and
Keenable, routed web search, TrestleIQ, call_media and resources_list on MCP, provider
resources, the search log.
Conflicts, every one "both sides added": imports in call/service.py and routers/web.py; the
error-owner table in call/types.py; dev-local.sh keeps the TREG_HUB_ENABLED passthrough in
front of main's new SERVER_ENV; the legacy dashboard keeps both the Resources and the Hub
nav entries and view names; test_mcp.py lists main's two new tools and the hub's three;
test_marketplace_call.py keeps both new test blocks; .gitignore keeps both. MAP.md and
docs/context/README.md regenerated. tests/test_dashboard_markup.py removed, as on main.
The hub's six migrations move from 0041-0046 to 0044-0049 above main's 0043; a fresh database
upgrades to one head, 0049.
SQLModel 0.0.45 rejects naive datetime bind parameters and may return
aware datetimes from the DB, breaking three crons:
- treg-asynctasks-settle: ValueError on WHERE next_check_at <= :now
- treg-catalog-stats: TypeError on created_at >= since comparison
- treg-arena-insights: ValueError on INSERT scan_until bind
Short-term fix: pin sqlmodel>=0.0.22,<0.0.45 → lockfile uses 0.0.44
Long-term prep: NaiveUTC annotations in place for eventual upgrade
Changes:
- Pin sqlmodel>=0.0.22,<0.0.45 in server extra
- Import NaiveDatetime from pydantic, define NaiveUTC type alias
- Update all datetime field annotations in models.py to use NaiveUTC
- Add regression tests for all three affected crons
Fixes production crashes in settle, catalog-stats, and arena-insights.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jason Zhou <JayZeeDesign@users.noreply.github.com>
main moved 210 commits ahead in five days (413 files: AnyAPI, Prospeo, Dropleads and other catalog
listings, routed-parent adaptation, the archive's own-key and repeat-pricing work, video task
settlement, org slug history). Only ONE file conflicted, the GENERATED MAP.md, which was rebuilt
rather than merged by hand. Nothing in the hub needed a code change this time.
Migration numbers collided again: main took 0039 and 0040, so the six hub revisions move
0039-0044 → 0041-0046 and chain onto main's 0040. Single head 0046; the chain applies clean on a
fresh sqlite with both hub switch columns present. The numbers named in architecture/hub.md follow,
and every file it names exists.
Verified after the merge: our phase 10 work survived intact — `scripts/build_plugin.py` still ships
the hub section unconditionally, and all five plugins still carry the hub (9 mentions each), the
"per-deployment switch" instruction for the off case, and no raw markers. main changed skill.md and
llms.txt; the plugin freshness check passes against the merged agent files.
Green: full suite 4393 passed, 8 skipped; hub + callmatrix + mcp 213; agent pages + seo 228;
surface snapshot + alembic parity 8; import-linter 14/14; plugin check OK. One failure in the
parallel run, test_prospeo_platform_email_settles_one_credit, which passes alone and whose whole
file passes alone (363); dev/hub touches no prospeo or marketplace file.
Closes#557
Root cause: PyPI's 0.19.1 was published before the `treg host` command
landed on main (c8d51c5e), but the skill served from the live server
documented `treg host`. Users installing via `curl | sh` got a CLI
without `host` and a skill that claimed it existed. Worse, because
`host` is also the name of a system DNS tool (/usr/bin/host), the
bare-word fallback (`treg claude` → `treg with claude`) intercepted
`treg host face.jpg` and ran `/usr/bin/host face.jpg` instead,
producing a confusing DNS error.
Changes:
- Bump version to 0.20.0 (0.19.1 is already on PyPI without `host`)
- Add test confirming `host` is a registered subcommand and won't fall
through to the system binary
- Add version note to skill.md (and all plugin copies): "Requires CLI
≥ 0.20.0; run `treg update` if `treg host` is unrecognised."
The existing `treg host` implementation is unchanged — it was already
on main, just never released.
Co-authored-by: Jason Zhou <JayZeeDesign@users.noreply.github.com>
main had moved 150 commits ahead. Nine files conflicted; every one keeps BOTH sides, except the
catalog search hint, which keeps the hub branch and calls main's new `_run_hint` helper for a
catalog row. The app serves the hub routes and main's new media routes; the worker keeps both its
hub and arena subcommands.
Migration numbers collided a second time: main took 0033-0038, so the six hub revisions move to
0039-0044 and chain onto main's 0038. Single head 0044; the chain applies clean on a fresh sqlite
with both hub switch columns present. The numbers named in architecture/hub.md follow.
Two real breakages the merge caused, both fixed here:
1. Money. main made the signup credit verified-only (`claim_signup_promo`), and `POST /users` is
legacy registration, which leaves the user UNVERIFIED. Every hub test buyer therefore started at
zero and each paid run answered 402. New shared helper `conftest.funded_user()` grants the team
1_000_000 micro; used at the 19 call sites that have to pay. The tests that deliberately exercise
the out-of-money path are untouched.
2. Identity. main added `ApiKey | None` to `Caller` for managed keys. The hub's scheduled check
builds a Caller directly and crashed with a TypeError. It now passes `api_key=None`: a scheduled
check is not an API-key call, it runs as the maker's membership.
Green on the merged tree: full suite 4070 passed, 8 skipped; agent pages + seo 228; import-linter
14/14; plugin check OK; alembic parity 4. `hub_enabled` stays False by default, and with the flag
absent every hub route answers 404 with a valid request, the agent files carry no hub text, and
catalog search returns no hub row.
Bring the team's work since 2026-09-09 into the hub branch: the review/feedback system, the arena
insights, the R2 body storage and cache admission for the archive, the shared key-value store, and
the SSRF CGNAT fix. 108 commits, 233 files.
The one real conflict was the migration numbers: both branches used 0026-0030 for different
changes. main's chain is 0026 (callreview) through 0032 (archive body storage); the five hub
migrations are renumbered to 0033-0037 and rechained onto 0032, so there is a single head 0037.
Everything else was additive and kept from both sides: the HubTool/HubRun models beside
CallReview/FeedbackHandling, the `hub` domain and the key-value store both in the AGENTS.md tables,
the hub CLI family beside `treg review`, the hub MCP verbs beside `review` in the tool sets, the
quickjs and obstore/redis dependencies together. mcp_feedback.py was removed on main; the deletion
is kept. The docs fragment's migration numbers were updated.
Full suite 3508 passed; agent pages + SEO 216; boundaries 14/14; the migration chain a single head
0037 on Postgres 16; the plugin staleness guard green.
The CLI now prints one stderr line for X-Treg-Hint: feedback beside the charge line, as it already
did for review invitations, and reads the unified header a 0.19 registry sends (the older
X-Treg-Review: requested still works against a pre-0.19 registry). Plugin manifests, the skill
copies and the lock file carry the same version.
Render Key Value (Redis protocol) at TREG_KV_URL, or a bounded in-process dictionary when unset.
One narrow method, take(key, limit, ttl_s): an atomic INCR + EXPIRE NX window, capped at 100 ms,
failing closed so a store that cannot answer is a spent budget rather than a flood. /admin/kv
(superadmin) reports configured/reachable; startup warns when the configured store is silent.
The first tenant is the review-invitation budget; TREG_REVIEW_BUDGET_PER_HOUR (default 5) is
declared here for it.
A script recipe's run.js now runs end to end (docs/HUB-DECISIONS.md round 2).
- treg/hub_sandbox.py, the CHILD: QuickJS (the `quickjs` package, hand-added to the server
extra and uv.lock; cp312 wheels for production, sdist locally) in a separate short-lived
process with no network, no file system, no require, no process, no timers. A JSON-lines
bridge over stdin/stdout: ctx.call -> parent -> result | refused; ctx.log; done | error.
64 MB engine heap; a memory bomb reads `memory`, an endless loop `timeout`, a throw `script`.
- application/hub/sandbox.py, the PARENT: runner.py's discipline - scrubbed env (never the
server's), private temp HOME, own process group, rlimits (CPU, fsize, no core, RLIMIT_AS on
Linux), a wall-clock kill of the whole group, cleanup on every exit. Enforces what the engine
cannot: the 20-call cap, the log caps, the output shape.
- runner.py, the script road: every ctx.call is the same child call the JSON road makes -
`uses` enforced per call (a URL, an unknown id, or a tool outside the list refuses and stops
the run), the run ceiling, {run}:s{n} holds, a catalog call as the CALLER, an own-tool call as
the MAKER. The returned object must carry output.fields (424 hub_output_invalid); a failed run
is 424 hub_run_failed {hub_script_failed, kind, message, trace, log, charged_micro}.
Two engine facts learned and documented: quickjs cannot call into Python while its own time
limit is set, so the wall clock is the parent's kill; ctx.call is synchronous underneath, so
Promise.all serializes in version one (the JSON road has the real parallelism).
Tests: 13 hostile sandbox cases (no road out, refusal stops the run, memory bomb, endless loop,
throw, missing export, non-object return, log cap, call cap, scrubbed env, parallel runs do not
mix) + 4 end-to-end script runs on the real call path with the four books. 72 hub tests; suite
2900; hub + serial list on real Postgres 16.
Security review of the sandbox is scheduled as its own pass before release (the owner's note).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(agents): AGENTS.md is the single guide, contract first, no state snapshots
CLAUDE.md now only imports AGENTS.md so Claude Code, Codex and Cursor read one file.
AGENTS.md is rewritten around what an agent cannot learn from the code:
- Non-negotiables move to the top and are corrected against the code. "Own key
always wins, never metered, never routed or overflowed" is promoted to rule 1.
The relay rule now scopes to plain /call/ and names routed endpoints and
overflow as wrappers, since route.py injects `_treg` into the body and overflow
adds X-Treg-Served-Via; the old wording contradicted both. The hold rule
defines "hold"; the pool rule says why reserve and settle are two transactions;
the ledger rule records that there is deliberately no refund entry.
- The dataplane write allowlist becomes guidance that points at
tests/test_call_architecture.py as the authority instead of a prose copy.
- Enforcement gap inventories, per-file commit lists, contract-by-contract
recaps and the CLI-module list are removed: they duplicate pyproject, the
import-boundaries fragment and the tests, and rot without a drift check.
- Endpoint and provider counts are removed (three files carried three values).
- Duplicated statements (own-key, faithful relay, keep-four-in-step, money-only
path, relay guard, secrets) are stated once each.
- Sections reordered: contract, where the truth lives, architecture,
development, working agreement, and a closing section for user-facing copy.
* build(uv): pin required-version instead of banning `uv lock`
uv.lock is revision 3, first written by uv 0.8.4. An older uv reads it fine but
rewrites it to revision 2 on any touch, dropping every upload-time field: the
~650-line no-op diff the old "always --frozen, never uv sync or uv lock" rule
worked around. That rule also told agents to hand-edit the lock, which --frozen
would then install unchecked.
- pyproject.toml: `[tool.uv] required-version = ">=0.12"`, so an old uv refuses
to run instead of rewriting the lock.
- ci.yml: `--frozen` becomes `--locked`, so a stale lock fails CI instead of
being installed silently. The comment about the team's older uv is gone.
- CONTRIBUTING.md names the floor; AGENTS.md replaces the ban with "change
dependencies through uv add or uv lock, never by hand".
- docs/context/architecture/import-boundaries.md describes the CI step as it
now runs.
Verified: uv lock --check, uv sync --locked and uv run --locked lint-imports
(12 kept, 0 broken) on uv 0.12.3.
Conflicts resolved:
- resolve.py: main's authorization-method ladder (chosen_tool / chosen_secret / methods) kept,
with the frozen settlement basis and async descriptor computed before `common` as before.
- routers/call.py: main moved the access dry-run to application/call/access.py; the price-range
and usage-settlement wording is ported there.
- Migration renumbered: main shipped 0010 (oauth authorization_method), so the async task table
is 0011 on top of it; docs and models references updated.
- catalog_validate: PARAM_MULTIPLEXED is the union (instagram + minimax + openrouter).
- catalog_ingest: main's carried-field tuple, still gated by carry_capability for `capability`.
- cli: both `-p/--query` and `--authorization-method`.
- Fragments (catalog, auth-secrets, data-model) keep both sections; MAP regenerated.
sqlite: 2543 passed. Postgres (CI subset, local postgres:16): 269 passed.
- A price table prices out as a range everywhere. The store computes the cheapest row into
cost.table_min at load time and cost_view exposes usd_min beside usd (still the validated
ceiling that reserve and eligibility read). The wall, `treg catalog search`, the dashboard
and /access now show "$0.2-$1.96/success" for MiniMax H3 instead of the $1.96 ceiling alone,
which had made direct routes look several times dearer than they are.
- The 24-hour deadline releases the hold instead of settling it at the reserve: an outcome
nobody observed is the platform's cost, never the customer's. The row is flagged for
reconcile (`absorbed_timeouts`) and the worker logs an ERROR-level alert naming the endpoint,
since the usual cause is a provider that silently changed its status field.
- One reading of the async descriptor. The CLI's --await now imports json_path,
classify_terminal and artifact from treg.domain.asynctasks (stdlib-only) instead of carrying
its own copy; the module joins test_import_lightness and gets an import-linter contract that
forbids every server root, so the light install can never grow a database import that way.
artifact() returns the fetch target as {endpoint, name, value}; callers format the command.
- An endpoint's async block replaces the provider default whole (effective_async_descriptor);
the recursive merge with mode pruning had one user, which overrode everything anyway.
- The usage unit "token" is gone from the validator and settle() until a metered token-priced
listing exists with its fx rule and a live test; its unit derivation was wrong (it ignored
`per`) and no real traffic had exercised it.
- AsyncTaskRecord.request_data dropped: settlement_basis already freezes the request with the
rule. The migration is unreleased. The dead observed_override in the defer-failure branch
is gone; that branch settles the frozen basis, which is what it always did.
Fragments (catalog, money, import-boundaries, cli) updated with the code; 2468 tests pass.
The PR4 sweep renamed three loggers to module paths - treg.ledger, treg.proxy, and
treg.billing became treg.domain.money, treg.infra.upstream.relay, and
treg.application.billing - an undeclared behavior change on exactly the settle/release,
cancel-release, and cap-check failure paths where external log filters and alerts hook in.
Reverted to the short functional names the rest of the pipeline uses (treg.idempotency,
treg.capacity, ...): a logger name is an observability contract and does not follow code
location. Also: the area:proxy labeler glob pointed at deleted proxy.py (the label could
never fire again) and area:auth-secrets listed the two deleted shims for injection and
session signing; the stripe dependency comment and a money.md passage named deleted files.
Add playwright for mobile viewport testing. Also add tests/screenshots/
to .gitignore as these are regenerated on demand.
Co-authored-by: Jason Zhou <JayZeeDesign@users.noreply.github.com>
The adoption release - migrations now execute through Alembic.
Changelog:
- python -m treg upgrade: the explicit release phase (schema + idempotent release tasks);
the default serve path runs it before uvicorn, so self-hosted stays one-command
- first boot on an existing database adopts it: frozen legacy init_db to terminal state,
a full table/column sweep, then alembic stamp head - incomplete schemas refuse by name
and never stamp; this version is the version floor a lagging install must upgrade through
- migration scripts ship inside the wheel (src/treg/alembic/); a pip install can migrate
- raw-ASGI operators must run python -m treg upgrade once per release (ops/deploy.md)
- role startup manifests no longer write data (companion backfill and single-user
provisioning moved to the release phase / serve pre-phase)
- routing: a provider that cannot express a supplied filter never wins on price (#254),
people-search keyword coverage (#256), routed discovery is a runtime switch (#257)
- the refusal messages carry their remedy: the adoption-window instruction names
tools-registry[server]==0.14.* and a rollback past the floor gets a named error
Ships the routed-endpoint CLI to PyPI/brew — the server half has been live since #242.
Changelog:
- treg catalog search groups a capability under its routed parent (5 children + '+N more'),
matched children pull their parent in, --limit hint when results are cut
- treg catalog get on a treg.* row: ROUTES AMONG plan, ALSO (same job, not routed — call by id),
RUN IT uses the identity variant most providers accept
- treg call --header for route options (max-cost, waterfall, prefer/exclude)
- treg org overflow on|off (team opt-out from aggregator overflow)
- skill.md + plugins regenerated (routed endpoints, overflow disclosure)
Build fix, found by the sdist leak scan on this build: hatchling ships every file git does not
ignore, and .git/info/exclude is local-only — docs/evidence/overflow-map-2026-08-26 (127 MB of
aggregator catalogs and probe dumps, naming partners we do not name publicly) would have been
published. sdist now carries an explicit exclude list; .gitignore states the same for git.
Tarball 166 MB → 19 MB.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeYKFQQpvESoBUX4WuUkZy
Step B of docs/PROVIDER-CAPACITY-PLAN.md. The 27 balance collectors move byte-identically from
scripts/provider_balances.py into domain/capacity/collectors.py (the script is now a thin
reconciliation CLI over them). New tables capacitypolicy + capacitysnapshot (create_all legacy
path + alembic 0002, parity-tested). `treg-worker capacity sweep` (new console script, server
extra, worker profile) collects → snapshots → publishes per-provider latest state to ratestore
(capacity:state:*); domain/capacity/view.py loads it on a 60 s TTL. Observe-only: no alerts,
nothing on the call path reads it yet. New import-linter contract keeps the domain a leaf.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeYKFQQpvESoBUX4WuUkZy
Credential-binding validation, secret ownership, local-run profile and
inject checks, bundle org-scoping, and bundle file hygiene move from
routers/resources.py into domain/tools (bindings.py, bundles.py) with
framework-neutral ToolConfigError/SecretOwnershipError. The router keeps
same-named wrappers that supply the caller facts and injector vocabulary
and translate the errors back into the identical 422/403 shapes, so the
HTTP surface and api.py's re-exports are unchanged. A new import-linter
contract pins the tools domain to the sanctioned tools->connections edge.
catalog_store.py -> domain/catalog/store.py, endpoint_stats.py ->
domain/catalog/stats.py, with compatibility facades at the old paths.
The YAML data stays at src/treg/catalog/; the store now anchors
CATALOG_DIR at the package root instead of module-relative. A new
import-linter contract pins the catalog domain as a leaf.