Skip to content

feat(quoter-bot): ship flat monitoring event vocabulary - #170

Open
haydenshively wants to merge 12 commits into
mainfrom
feat/quoter-bot-monitoring-events
Open

feat(quoter-bot): ship flat monitoring event vocabulary#170
haydenshively wants to merge 12 commits into
mainfrom
feat/quoter-bot-monitoring-events

Conversation

@haydenshively

@haydenshively haydenshively commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Implements TIB-2026-08-23-quoter-bot-monitoring-events. Closes BOTS-86.

Problem

quoter-bot already had the Better Stack transport — createBotObservability, the heartbeat, process
observers, the both-or-neither opt-in. What it lacked was a vocabulary. bot-observability.utils.ts
defaults any record without an event field to bot.action, and nearly every per-cycle report the
bot emitted lacked one, so Better Stack received nested LadderRunResult[] blobs that no metric
expression can aggregate. This is a what-to-emit change, not a plumbing change.

What ships

A typed MonitoringEvent union (17 variants, schemaVersion: 1 bound into the logger context) with
flat scalar payloads and bounded grouping dimensions. Records are projected from cycle results at the
application layer — src/domain/ stays pure and gets no logger. Guardrails that were deliberately
made non-throwing (rate clamps, cross-book clearance, rung truncation, exposure caps) are now
visible, aggregated per side per cycle rather than per rung.

Fills come from per-group consumed deltas, which are monotonic per group ID, rather than from
diffing the active quote set — reconcile churns group IDs on every recenter, so a quote-set diff
conflates taker consumption with the bot's own cancel/replace.

Only allowlisted names reach Better Stack. isShippableRecord carries a compile-time exhaustiveness
guard against the union, so a new variant that is not allowlisted fails typecheck; quoter-bot.cycle
and readonly.make stay local to stdout. The four lifecycle records (bot.started, bot.stopped,
bot.unexpected-error, heartbeat.failed) ship from the observability layer and bypass this
boundary.

Zero additional RPC. Every field is a projection of data already read, including maturityTimestamp
(derived from the groups the ladder already fetches) and the accounting primitives that
calculateLadderCapacities previously consumed and discarded.

Notable decisions

  • bot.failed — a readiness failure throws before any monitor loop starts, and the fail-together
    lifecycle can stop without any cycle record explaining which workflow ended. Without this record
    the shipped stream shows a start and a stop with nothing between them.
  • market.configured per market — one process-wide minimum cadence would make every slower
    market look overdue. Covers the union of ladder and bootstrap markets so a bootstrap-only market
    stays in scope for absence alerting.
  • Single combined readreadActiveState returns the quote and its consumption together, and is
    sampled before reconcile forgets replaced groups. An earlier version deduped two reads with a
    single-flight helper; that was timing-dependent, so the port was changed instead.
  • Monitoring cannot halt quotingCli.writeCycle writes the raw record first, then projects
    inside a try/catch. Telemetry can be lost; a cycle cannot be stopped by it.
  • Halt cancellations are strategy-widehardHalt pulls every market, so its
    transaction.settled records omit marketId even though the halted result names the market that
    triggered it. Only the market-scoped market-read-failed reconciliation keeps marketId.

Docs: event tables, the units/cardinality contract, alert recipes, and Known limits in
bots/quoter-bot/README.md and bots/quoter-bot/docs/reference.md. Addendum A on the TIB records
every delta between the accepted design and what shipped.

Review

Three passes, each catching a distinct class of defect: a code review (shipping allowlist too
permissive, dropped terminal reports), a design review for necessity/sufficiency/simplicity (cut
derivable fields, book.observed silent in the empty case), and a contract audit that verified each
property this PR asserts end-to-end (the timing-dependent dedup, monitoring on the cycle path, and
six documentation claims that did not match the code).

Automated-review follow-ups: position.observed/book.observed now describe the same post-check
snapshot, an expected MakerAccountError ships one sanitized bot.failed instead of nothing,
cleanup-failed reports carry cleanup.errorName, and ladder hard-halt cancellations keep their
confirmed hashes. Projecting one-shot runOnce results through writeCycle was declined — that path
prints its full report to stdout, so it would interleave derived records into operator output.

Verification

pnpm --filter @morpho-org/quoter-bot run typecheck · pnpm lint (0 warnings) · pnpm test
(77 files; 2 RPC-fork E2E suites need RPC_URL_8453) · knip · scripts/check-jsdoc.ts — all clean.
New guards were verified by breaking one assertion and reverting.

Known CI gap: the Test job fails at foundry-rs/foundry-toolchain setup
(foundryup: cannot execute binary file) before any test runs. The same failure is on main
(b5624de) and this branch does not touch the workflow.

Link to Devin session: https://app.devin.ai/sessions/aff4124e02814ac89fb7e6520ecc82c1
Requested by: @haydenshively

haydenshively and others added 4 commits August 24, 2026 00:21
quoter-bot already had the Better Stack transport, but nearly every
per-cycle report lacked an `event` field and shipped as the `bot.action`
catch-all carrying nested `LadderRunResult[]` and `SetupCheckReport`
blobs. No metric expression can aggregate that shape, so the only
working alert was "the bot crashed."

Implements TIB-2026-08-23-quoter-bot-monitoring-events: a flat, named,
low-cardinality vocabulary projected from data the bot already reads.

- Domain stays pure. `generateLadderWithDiagnostics` and
  `decidePositionBootstrapWithDiagnostics` return clamp, clearance,
  truncation, and size-cap counts alongside their results; the existing
  entry points become thin wrappers with unchanged behavior.
- `calculateLadderCapacities` also returns the balance primitives it
  previously discarded. The four capacities are saturating minima and
  cannot be inverted into a position value, so PnL needs the inputs.
- Fills come from diffing monotonic per-group `consumed`, not the active
  quote set: reconciliation reserves fresh group IDs on every recenter
  and resize, so a quote-set diff conflates takers with the bot's own
  churn. Adds an optional observation-only `readConsumption` port.
- Projections run at the four CLI cycle seams where the workflow is
  known statically, and ship through the existing `writeEvent` seam so
  stdout and Better Stack stay identical.
- `bot.configured` anchors absence alerts; `schemaVersion` binds into
  logger context only when shipping is fully configured, preserving the
  both-or-neither opt-in and zero-network default.

Monitoring adds no RPC round trip: `maturityTimestamp` is projected from
groups already read, and consumption reuses the ownership read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quoter-bot now imports createLogger, railwayContext, and
classifyShippingConfig directly to bind schemaVersion into the shipping
logger context, so the dependency is real and knip can see it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adversarial review found two gaps against the TIB.

Fill telemetry added a real provider round trip. `readConsumption` calls
`readGroups`, which was a plain function hitting the Morpho API on every
call, and `completeResult` awaited it — so a verbose cycle paid an extra
request per market and delayed the quoting loop. `readGroups` is now
single-flighted, so the consumption read collapses into the
active-quote read it already runs concurrently with. Deduplication is
concurrent-only, so every fresh call still reads current group state.

The `bot.action` fallback was still reachable. Raw cycle arrays and
terminal monitor reports shipped without an `event`, so Better Stack kept
receiving unaggregatable nested blobs alongside the new vocabulary.
Shipping now filters to named records only. Terminal output is
byte-identical, and the dropped content survives as `cycle.completed`,
`guardrail.halted`, and the lifecycle events.

Also corrects the reference's no-additional-RPC claim, which overstated
what the consumption path does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`guardrail.spread-rejected` keyed off the sanitized `BootstrapAdapterError`
classification, which collapses 32 distinct adapter operations into one
name. Any adapter failure — a provider response, a transaction policy
rejection, a stale reference — tripped a cross-book spread alert, masking
the real failure class.

Adds `operatorAdapterOperation`, an allowlist projection of the adapter's
specific operation, and threads it onto the bootstrap failure and halt
outcomes alongside the existing `errorName`. The guardrail now keys on
`adapterOperation === 'negative-spread'`, so it fires only on an actual
cross-book rejection. Both paths a negative spread can surface through
are covered: a reconcile throw and a preview throw.

The operation field is typed `string` on the error, so it is allowlisted
rather than passed through — an operator-visible dimension must never be
able to carry provider text. Unrecognized values are withheld.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@haydenshively haydenshively self-assigned this Aug 25, 2026
haydenshively and others added 2 commits August 25, 2026 00:43
An observability design review flagged that the implementation violated
its own boundary, shipped redundant records, and missed the two incidents
operators most need explained.

Shipping is now an explicit allowlist rather than "any record carrying an
`event`". The previous predicate admitted the nested `quoter-bot.cycle`
envelope and `readonly.make`, so unversioned blobs still reached the log
source under a different name. A compile-time exhaustiveness check fails
the build if an event is added to the union without being listed.

Adds `bot.failed`, the only signal for two failures no cycle record can
describe: a readiness check that fails during startup before any monitor
loop begins, and a fail-together lifecycle where one supervised workflow
ends and stops its peers. The combined case emits one record per failed
workflow, so "which workflow half-broke?" is answerable. Filtering
shipping to named records had removed this information entirely.

Fixes "not quoting", which produced silence: `book.observed` was skipped
whenever no quote was active, so `state: 'empty'` only fired when a quote
already existed. Both sides are now reported on every observed cycle.

Splits per-market cadence into `market.configured`. One process-wide
shortest interval made slower markets look overdue to absence alerts.

Cuts fields that restate other fields or a constant: `setup.ready`,
`bootstrap.progress.shortfallAssets` and `.mode`,
`guardrail.cross-book-cleared.clearanceBps`,
`guardrail.spread-rejected.errorName`, `transaction.settled.status`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`market.configured` mapped only the ladder strategy, so a market
configured for bootstrap alone appeared in no manifest record once
`bot.configured` stopped carrying `marketIds` — leaving it outside every
absence alert. It now covers the union of both strategies, with
`ladderIntervalSeconds` omitted for a bootstrap-only market whose cadence
is the process-wide `bootstrapIntervalSeconds`.

Also drops `mode` from the documented cardinality allowlist: no event
carries it since `bootstrap.progress` lost the field, and records that
the manifest is emitted by `start` rather than the standalone
operator commands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@haydenshively

Copy link
Copy Markdown
Collaborator Author

@codex review

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread bots/quoter-bot/src/application/bootstrap/position-bootstrap.service.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a52c97567b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bots/quoter-bot/src/infrastructure/ladder/production-ladder.ts Outdated
Comment thread bots/quoter-bot/src/application/monitoring/ladder-monitoring.utils.ts Outdated
Comment thread bots/quoter-bot/src/domain/bootstrap/position-bootstrap.ts
Comment thread bots/quoter-bot/src/infrastructure/ladder/ladder-capacity.utils.ts Outdated
Comment thread bots/quoter-bot/src/application/monitoring/ladder-monitoring.utils.ts Outdated
Comment thread bots/quoter-bot/src/application/bootstrap/position-bootstrap.service.ts Outdated
Comment thread bots/quoter-bot/src/application/monitoring/ladder-monitoring.utils.ts Outdated
Comment thread bots/quoter-bot/src/application/monitoring/bot-configured.utils.ts
haydenshively and others added 2 commits August 25, 2026 01:20
Eleven review findings; ten were real.

Fill telemetry was lost on the cycles that matter most. Consumption was
sampled inside `completeResult`, which runs after `reconcile` has already
forgotten a replaced group from durable ownership — and a fill is exactly
what causes the resize that replaces it. It is now sampled alongside the
active-quote read, before reconciliation, sharing that read's deduplicated
groups request.

`book.observed` described the book before reconciliation, so a successful
first publication reported `state: 'empty'` and a successful invalidation
reported the old book as `quoting`. It now projects the post-check read,
falling back to the pre-check one when that read did not succeed.

`bestRateBps` took the maximum on both sides, but the sides run in
opposite directions from the center, so every higher-side gauge was
inverted — the outermost rung was labelled best.

The fill baseline could regress. An eventually consistent replay of an
older `consumed` lowered the baseline, so the next fresh value re-emitted
the already-counted portion. Baselines now only advance, and are evicted
after 500 unseen cycles so the map stays bounded as reconciliation keeps
reserving fresh group IDs.

Bootstrap `durationMs` spanned both passes of `runOnce`, so an early
market's duration included every later market's read. It now measures
only that market's own planning, mutation, and post-check work.

Also: `cashBalanceAssets` reported `min(balance, allowance)` rather than
the wallet balance it documents; `cappedAssets` could go negative against
its unsigned contract; `market.configured` could not distinguish a market
missing a bootstrap cycle from one never configured for bootstrap;
`adapterOperationField` sat in a file containing a class, against the
utility-isolation convention; and `groupRateBps` was documented as exact
under `shared-rung` despite tick alignment moving the published rate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A contract audit checked twelve properties this change asserts about
itself and disproved eight. This closes the ones that were real and
corrects the prose for the ones that were only claimed.

Monitoring could still add a provider round trip. The consumption read
and the active-quote read raced a single-flight slot that is released the
moment the first request settles, so deduplication was timing-dependent —
`readActive` awaits `cleanupRemovedMarkets` first, giving the consumption
request time to finish and free the slot. One adapter call now derives
both the quote and its groups' consumption from a single groups read, so
the guarantee holds by construction rather than by scheduling.

Monitoring could halt quoting. Monitor loops await the cycle callback
while holding the combined writer queue, so a throwing projection or a
failing sink surfaced as a failed cycle. Deriving and writing the derived
records is now isolated: telemetry can be lost, the bot cannot be stopped
by it. The cycle value keeps its unguarded write so genuine output
failures still surface.

Prose corrected where the code was right and the claim was not: terminal
failure reports reach stderr, not stdout; the shipping opt-in silences log
records but not the independently configured heartbeat; the allowlist
governs records flowing through the CLI writer, while the four lifecycle
records reach the logger directly; and `adapterOperation` is an internal
discriminator, not a shipped dimension.

Two alert recipes were unsound. Crash now covers `bot.unexpected-error`,
which an unclassified entrypoint failure emits without any `bot.failed`.
Stale reference now records that a bootstrap-only market can skip the rate
read entirely, so absence there means "not needed", not "stale".

Adds a TIB addendum recording every delta from the original event table
rather than editing the accepted decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@haydenshively
haydenshively marked this pull request as ready for review August 25, 2026 06:48
@haydenshively
haydenshively force-pushed the feat/quoter-bot-monitoring-events branch from c1aa10e to 252954a Compare August 25, 2026 06:51

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1aa10e829

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bots/quoter-bot/src/application/monitoring/ladder-monitoring.utils.ts Outdated
Comment thread bots/quoter-bot/src/infrastructure/cli/quoter-bot-entrypoint.ts
Comment thread bots/quoter-bot/src/infrastructure/cli/quoter-bot-entrypoint.ts Outdated
haydenshively and others added 2 commits August 25, 2026 14:54
…oring-events

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@julien-devatom

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6ce8fcbbe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread bots/quoter-bot/src/infrastructure/cli/cli.ts
Comment thread bots/quoter-bot/src/application/monitoring/terminal-monitoring.utils.ts Outdated
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@linear-code

linear-code Bot commented Aug 26, 2026

Copy link
Copy Markdown

BOTS-86

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants