* Meter telegram and buzz turns, closing the transport-contract ledger
Slack and Discord consume credits before running a turn; telegram and buzz
did not, so two of the four transports ran every turn for free. Both were
carried in the known-gaps ledger in test_transport_contract.
Add the credit gate to both inbound handlers at the same point Slack and
Discord place it — after the session resolves, before any turn work — and
empty the ledger, so the contract test now requires metering of all four.
The charge goes to scope.principal.id (the silo org), not the chat id or
sender pubkey. Only an explicit 402 blocks the turn: UNCONFIGURED and
UNAVAILABLE proceed, so a billing outage never silences the bot and a config
error never reads to users as "out of credits".
The call is awaited off-thread. The Slack/Discord dispatchers are already on
worker threads, but these two are coroutines, and buzz dispatches turns as
concurrent tasks on one loop — a synchronous POST would park every other
in-flight turn, and the poll loop with them, for the client timeout.
Gateway tests now clear the metering env, since a developer whose shell
exports it would otherwise have the suite POST charges to a live ledger. The
machine secret goes too: the bearer token is minted from Clerk before the URL
is checked, so clearing the URL alone still calls out.
* Charge synchronously so a cancelled turn cannot strand a credit
Greptile P1 on #5388. Awaiting the charge off-thread made it a cancellation
point. Shutdown cancels turns that outlast the drain budget, and _dispatch_turn
catches Exception, which CancelledError is not — so the coroutine unwinds past
the trailing acknowledge(). The POST still completes on its worker thread, the
mention stays uncommitted, and the next start re-delivers it and charges the
organization a second time for one message.
Reverting to the synchronous call closes the window. Nothing suspends between
the charge and the run_in_executor handoff, so the turn body is always queued
once the ledger has been debited, and its on_handled acknowledges from the
executor thread the way the existing docstring describes.
This also puts both handlers back on the Slack/Discord shape the issue asked
for. The loop-parking the off-thread call avoided was speculative; the
surrounding code already blocks the loop with the session resolve and, on the
denial path, a buzz CLI subprocess spawn.
Add a regression test that cancels a Buzz turn while the charge is in flight
and asserts the mention is still acknowledged. It fails on the awaited version
and passes on this one.
* Charge from the turn body so the ledger blocks neither the loop nor the ack
Two review findings pulled in opposite directions. Greptile's P1 said not to
await the charge: a shutdown cancel landing on that await unwinds past
_dispatch_turn's acknowledge() while the POST completes anyway, stranding a
credit on a mention the next start re-delivers and charges again. Devesh36
then flagged the synchronous fix: up to 5s of the credits client timeout on
the event loop, stalling Buzz polling, dispatch and shutdown.
Both hold. Metering now runs inside the turn body, on the executor thread,
which answers each. Off the loop, a slow ledger cannot stall polling or the
other turns sharing it. Fused to the work it pays for, the debit cannot be
stranded: cancelling the awaiting task does not stop the executor thread,
whose finally still acknowledges the mention.
A denied turn now finalizes its placeholder with the out-of-credits message
rather than posting a second one, matching how these two handlers already
surface timeout and user-stop.
Tests: charging off the event loop, and a turn cancelled as the charge lands
still being acknowledged. Each fails against the shape it guards against.
* fix(gateway): meter only admitted chat turns
* fix(gateway): keep admitted turns fused to billing
* test(gateway): pin the pre-admission cancel branch, trim the duplicate
`run` returns None for three reasons now, and only the capacity gate
finalizes a message. Say so on the docstring, and record why admission
deliberately holds a slot it has not used yet.
Add the missing test for the cancel check that runs before admission, so a
turn stopped while queued can never reach a metering hook that debits.
Drop buzz's fail-open duplicate: the parametrized test in
gateway/tests/billing already covers every non-denied outcome. Scope the
transport contract's empty gap ledger to transports, since scheduled runs
reach the agent through scheduler_runners rather than a transport.
* test(telegram): meter on the production dispatch path
Telegram now registers the cancel Event before the turn exists (#5218), so
the metering test was exercising a branch production no longer takes.
* test(cli): pin the generic Anthropic model-not-found message
f3053e2c5 stopped copying str(exc) into the Anthropic 404 user message so
internal model ids stay out of investigation reports, but this older CLI
test still asserted the model id was echoed. Assert the hardening instead.
Infrastructure
infrastructure/ contains shared runtime services that sit outside the user-facing
application package and outside the core agent harness loop.
Infrastructure code may own side effects such as telemetry emission, audit logging,
runtime display, tracing, auth verification, masking, sandbox execution, and
minimal guardrails. Configuration-only behavior belongs in config/; agent
orchestration, state, tool planning, and tool execution contracts belong in
core/.
Name packages by what they do. Do not add a common/ / shared/ / util/
junk drawer. Prefer leaf imports over re-export shims.
Process & install
process/— exit codes, CLI runtime flags, process-wide turn capacity (process/turn_capacity/), and installed vs latest release version (process/release_version.py).setup_state.py— install/setup facts surfaced to agents and doctor.harness_providers/— integration provider registries wired into the harness.
Contracts
errors/—OpenSREErrorcontract (any layer may raise/catch).service_families/— tool-availability family-key normalization.
Prompt-sized results
text/— truncate / coerce / URL validation helpers.evidence/— log and evidence compaction; metric summary for tool results.
Observability & UX
observability/— logging, tracing, progress, debug output, and runtime display ports.logging/— shell/third-party log handlers.analytics/— product and runtime analytics.terminal/— terminal theme and display helpers.
Safety — safety/
safety/auth/— runtime authentication and identity checks.safety/guardrails/— minimal runtime safety checks outside the core agent loop.safety/masking/— reversible masking and identifier normalization.safety/sandbox/— constrained execution environments.
Scheduled and background work — scheduling/
scheduling/scheduler/— cron and agentic loop tasks, hosted by the gateway process. Not a gateway submodule: every surface reads and mutates the task store, and the gateway only supplies the process and the capacity gate.scheduling/task_types.py/scheduling/task_registry.py— in-flight task contract and persistent registry. (Not under atasks/directory — root.gitignoreignorestasks/everywhere.)scheduling/background_investigations/— background investigation store and types.
Delivery — delivery/
delivery/notifications/— notification delivery transports and channel-specific senders.delivery/reporting/— cross-vendor report-delivery registry and surface-agnostic ports.
Persistence
filestorage/— syncable file storage providers and operations.
Deploy and packaging — deployment/
deployment/ec2/— EC2 AWS primitives and Telegram gateway AMI/systemd deploy (telegram_gateway/). Makefile:make deploy-gateway.deployment/packaging/— wheel validation and release manifest helpers.deployment/contracts/— shared deployment models.SizeProfileis read at runtime by the gateway capacity gate, so this is not build-time-only code.
The Cloudflare Worker for install.opensre.com is not Python and lives at
deployment/cloudflare_install_proxy/.
Future migrations should move existing modules into this folder incrementally with import updates and tests. Avoid compatibility-only forwarding modules; each migration should leave one canonical import path.