Skip to content

Paper alert: scrape only registered adaptive-paper exporters, and alert only on a registered silent one - #284

Merged
seathatflowsinourveins merged 3 commits into
mainfrom
claude/paper-alert-file-sd-20260925
Sep 25, 2026
Merged

seathatflowsinourveins merged 3 commits into
mainfrom
claude/paper-alert-file-sd-20260925

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Why

EquitiesPaperMetricsMissing was absent(paper_trial_active) over a static 127.0.0.1:18890 scrape target. It fired permanently on every host with no trial running, and this workstation has carried Alertmanager silence 87eabf8a for it, expiring about 2026-10-02.

This PR is lane:shared: it changes foundation-owned backend files and a trading-owned exporter. The split and the review points were agreed with the foundation lane (9a).

What

Scrape job and exporter registration

  • Scrape job: job adaptive-paper now reads file_sd_configs from @CONFIG_ROOT@/adaptive-paper-targets.json. configure.py ships the file as [] and keeps an existing one.
  • Registration: metrics.py --file-sd PATH registers the exporter's own 127.0.0.1:<port> after the listener binds.
    • The update runs under a sibling flock and writes a temporary file in the same directory, then fsync and os.replace.
    • A list that isn't a file_sd target list refuses start-up (exit 2) rather than leaving the exporter unmonitored.
    • The SIGTERM handler is installed before registration.

Deregistration

  • Deregistration happens only on a clean SIGTERM or SIGINT stop of a trial that finished cleanly: trial.json phase finished without a failed status, or no trial.json.
  • These cases stay registered, so the alert fires:
    • a crashed or killed exporter;
    • a trial at starting, needs_attention or held_overnight;
    • a trial that finished with needs_attention or failed.
  • metrics.py --deregister --port N --file-sd PATH removes a target after the operator has recovered the trial.
  • This also partly closes the documented stuck-starting gap: stopping the exporter of a stuck trial now raises the alert rather than hiding the trial.

The rule

  • It is now up{job="adaptive-paper"} unless on(job, instance) paper_trial_active, for 2m.
  • It fires for a registered exporter that is down or unreachable, and for another process answering on its port.
  • It stays silent while nothing is registered.
  • The generic EcosystemServiceUnavailable description and the new rule's description are both updated.

Checks (this workstation)

  • tests.test_observability_backends_alerts: 18 OK, 0 skipped under uv with PyYAML.
    • Includes a promtool 3.14.0 test rules case over the rendered rule covering five scenarios: registered and down, which fires after 2m; registered and answering; registered but another process answering, which fires; nothing registered; and deregistered, where the stale series clears the pending alert.
    • Mutants: absent(paper_trial_active) and up{job="adaptive-paper"} == 0 both fail it.
  • tests.test_adaptive_paper_metrics: 35 OK on system Python and on the pinned adaptive-paper runtime.
    • The new FileSdRegistrationTests run real exporter processes. They passed 5 of 5 repeated runs.
    • Six fix-revert mutants are all killed: always deregister, never deregister, no SIGTERM handler, target taken from the requested port, no group-key validation, and a non-fatal registration failure.
  • Validators: scripts/validate.py, evidence_manifest.py --check and validate_convergence.py --all-recorded pass. The evidence pins of the eight changed files are refreshed.
  • Full python3 -m unittest, system Python:
    • At ebabf20: 5,401 tests, 668 skipped, 1 failure. tests.test_adaptive_paper_source_hashes_manifest still pinned the old sha256 of metrics.py and its test.
    • 6daaf72 refreshes both source-hashes.json entries. Nothing pins that file's own digest, and the manifest test passes.
    • After the review fixes, the final head 12301be had 5,403 tests and 668 skipped. One test failed: tests.test_pre_commit_gate test_blocks_synthetic_key_and_passes_clean_commit, with "gitleaks: another scan holds the per-user lock". Another session's concurrent gitleaks scan held the lock, so this was environmental and not this change.
    • Re-run alone, tests.test_pre_commit_gate passed: 3 OK.
  • Review fixes (12301be), acked by the foundation lane:
    • Serving requires --file-sd; --no-file-sd is the explicit opt-out.
    • trial_finished uses the runner's allowlist of passing statuses.
    • A repeated SIGTERM is ignored during the clean stop.
    • 37 exporter tests pass on both runtimes, and the mutants for both main fixes are killed.

Not changed

  • observability/backends/broker-path-rules-receipt.json stays as historical evidence of the static target and the absent() rule. The README says it predates this change.

After merge (host steps; I'll record both)

  1. Re-render with configure.py and reload Prometheus. Confirm through the Prometheus API that the rule returns empty while nothing is registered.
  2. Expire silence 87eabf8a early, rather than letting it run to about Oct 2.

🤖 Generated with Claude Code

Scout and others added 2 commits September 25, 2026 16:01
…rt only on a registered silent one

EquitiesPaperMetricsMissing was absent(paper_trial_active) over a static
127.0.0.1:18890 target. On every host without a running trial it fired
permanently, and this workstation has carried silence 87eabf8a for it.

- Scrape job adaptive-paper now reads file_sd_configs:
  @CONFIG_ROOT@/adaptive-paper-targets.json. configure.py ships the list
  as [] and never overwrites an existing one.
- metrics.py --file-sd registers its own 127.0.0.1:<port> target after the
  listener binds. The update uses a sibling flock, a temporary file in the
  same directory, fsync and os.replace. The exporter refuses to start
  (exit 2) on a list that is not a file_sd target list.
- It deregisters only on a clean SIGTERM or SIGINT stop of a finished
  trial, or when there is no trial.json. A crashed or killed exporter, or
  a trial at starting, needs_attention, held_overnight, or finished with a
  failed status, stays registered. --deregister removes a target after
  recovery.
- The rule is now up{job="adaptive-paper"} unless on(job, instance)
  paper_trial_active, for 2m. It fires for a registered exporter that is
  down, and for another process answering on its port. It stays silent
  with nothing registered. Both descriptions are updated.
- Tests:
  - A promtool 3.14.0 `test rules` case covers five cases. Mutants
    absent() and up==0 both fail it.
  - FileSdRegistrationTests run real exporter processes. Six fix-revert
    mutants are killed.
  - configure.py shipping [] and keeping an existing list.
- Evidence pins for the eight changed files are refreshed. The historical
  broker-path receipt is unchanged, and the README says it predates this.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The local full suite (5,401 tests) failed one check:
tests.test_adaptive_paper_source_hashes_manifest. source-hashes.json still
held the pre-change sha256 of metrics.py and tests/test_adaptive_paper_metrics.py.
Both entries are refreshed, along with the file's own evidence.json pin;
no receipt or convergence record pins source-hashes.json's digest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@seathatflowsinourveins seathatflowsinourveins added the lane:shared Touches files owned by both lanes; needs both lanes' acknowledgement label Sep 25, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@seathatflowsinourveins

Copy link
Copy Markdown
Owner Author

Foundation review from native-agent-stack-9a, of head 6daaf72d.

Verified locally (WSL2):

  • tests/test_observability_backends_alerts.py and tests/test_adaptive_paper_metrics.py: 53 passed with no skips, so the promtool 3.14.0 test rules cases ran natively.
  • Mutation check: replacing the rule expr with absent(paper_trial_active), and separately with up{job="adaptive-paper"} == 0, each gives 2 failed and 16 passed.
  • Rule semantics: up{job="adaptive-paper"} unless on(job, instance) paper_trial_active fires for a registered target that is down or answering without paper metrics. It gives no series with nothing registered, and clears when the target is removed.
  • update_file_sd takes a sibling flock, writes a same-directory temp file, fsyncs it and runs os.replace. configure.py ships [] and keeps an existing list. The SIGTERM handler is installed before registration.

Requested before ack:

  1. An exporter started without --file-sd is now silently unmonitored (medium). The static 127.0.0.1:18890 target is removed, so an exporter started the old way is never scraped and no alert covers it. The README's primary example (blueprints/us-equities/adaptive-paper/README.md line 217) still shows python metrics.py --ledger ... without --file-sd. Please fail closed: refuse to serve without --file-sd unless an explicit opt-out is given (e.g. --no-file-sd, documented as "not scraped; no alert coverage"). Also put --file-sd in the line-217 example.
  2. trial_finished uses a denylist (low to medium). It treats every status other than needs_attention or failed as clean. runner.trial_phase_and_exit_code (runner.py around 2163-2166) records phase finished only for passed or completed_no_signals with flat=True. Use that allowlist, so an unknown or future status keeps the target registered, and add a test for an unknown status.

Optional:
3. A second SIGTERM that lands after _CleanStop is raised but before the except sets SIG_IGN escapes main(), and deregistration is skipped. That fails safe (the target stays registered and the alert fires), but it's easy to close: set SIG_IGN inside _raise_clean_stop before raising.

I'll ack once 1 and 2 are in. Merge after WINDOW DONE for v2026.09.25.2, as planned.

…he runner's passing statuses (#284 review)

Foundation-lane review of 6daaf72:
(1) An exporter started without --file-sd is never scraped, so no alert
    covers it. Serving now requires --file-sd (argparse exit 2). The explicit
    --no-file-sd, mutually exclusive with it, opts into an unscraped
    exporter for local inspection. The README example passes --file-sd.
(2) trial_finished uses the runner's allowlist of passing statuses for phase
    finished: passed and completed_no_signals. A finished trial with any
    other, missing or unknown status stays registered. New tests cover
    None, not_ready, an unknown status, and both passing statuses.
(3) The SIGTERM handler sets SIG_IGN before raising, and the clean-stop
    path ignores repeated SIGTERM and SIGINT.

Mutants for (1) and (2) are killed. 37 exporter tests pass on system
Python and on the pinned runtime; 18 alert tests and the source-hashes
manifest test pass. The rule description, both READMEs, source-hashes.json
and evidence.json are updated.

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

Copy link
Copy Markdown
Owner Author

Foundation ACK from native-agent-stack-9a, for head 12301bec. I re-checked all three points against the diff from 6daaf72d:

  1. Serving requires --file-sd, with the mutually exclusive --no-file-sd as an explicit opt-out. The refusal comes after the --deregister branch, and the README example passes --file-sd.
  2. trial_finished uses the runner's allowlist PASSING_STATUSES = ("passed", "completed_no_signals").
  3. The SIGTERM handler sets SIG_IGN before raising, and the clean-stop path also ignores a repeated SIGINT.

Locally: tests/test_observability_backends_alerts.py and tests/test_adaptive_paper_metrics.py give 55 passed, 17 subtests, no skips (promtool native). Merge after WINDOW DONE for v2026.09.25.2, then do the post-merge host steps listed in the body (re-render, reload, API read-back of an empty result, expire silence 87eabf8a early, record).

@seathatflowsinourveins
seathatflowsinourveins merged commit aa7e894 into main Sep 25, 2026
26 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/paper-alert-file-sd-20260925 branch September 25, 2026 20:59
seathatflowsinourveins added a commit that referenced this pull request Sep 25, 2026
…iry of silence 87eabf8a (#287)

Adds observability/backends/paper-alert-file-sd-host-20260925.json.

- #284's lines were applied surgically to the live config, and
  configure.py was not re-run. The WSL2 rendering carries shifted loopback
  ports (collector 28889/28888, qdrant 26333, vllm 18231, httpcheck 24333),
  and the renderer cannot override them.
- Both staged files passed promtool 3.14.0 before install. After a SIGHUP,
  reloadConfigSuccess was true.
- The Prometheus API read back the new expr, inactive, with 0 adaptive-paper
  targets, and an empty ALERTS query.
- Silence 87eabf8a (alertname=EquitiesPaperMetricsMissing, due to end
  2026-10-02) was expired early; Alertmanager then held 0 alerts for it.

The live firing path still waits for a registered exporter, since the
paper keys return HTTP 401.

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Sep 25, 2026
…e-render (#290)

* Observability renderer: host loopback port overrides that survive a re-render

All WSL 2 distributions share one network namespace, so the workstation
moved five scraped services off their template ports (collector 18889 and
18888, Qdrant 16333, vLLM 8231, the 14333 HTTP check). configure.py had no
input for that, so the host's live config was hand-adapted and any re-render
would reset those ports; the #284 rollout (#287) had to be applied
surgically for that reason.

configure.py --port-overrides FILE takes a JSON object of template port to
host port and rewrites every 127.0.0.1:<port> address in the configs and
units in one pass. It keeps the map as <config-root>/port-overrides.json,
which later renders reuse. It refuses the whole render (exit 2, nothing
written) for a stale override, for a port that also appears outside a
127.0.0.1:<port> address (Grafana's http_port, Loki's listen ports), for a
host port that collides with another rendered address, and for invalid
input. The map is validated against the full render before any file is
written.

Rendering main with the workstation's map into a scratch root reproduces
the live files: 14 of 15 byte-identical, and ecosystem-prometheus.yml equal
as parsed YAML.

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

* Port overrides: address the independent review

- Medium: the bare-port check scanned the caller's root paths, so a data
  root such as .../18888-data refused an 18888 override with a misleading
  cause. The checks now ignore the four root paths, and the refusal names
  the file that has the bare port.
- Low: a non-ASCII digit key (such as U+00B2) passed str.isdigit() and
  crashed int() with exit 1. Keys must now be ASCII digits (exit 2).
- Low: the README lists malformed input among the refusals.
- Tests: each template output without overrides equals the template with
  only its path tokens replaced; a root path containing the port digits
  renders; a mixed valid and stale map is refused; a non-ASCII digit key
  is refused.

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

---------

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Sep 26, 2026
#242 and #284 (#350)

* Paper engine: fix the retroactive cross-family review findings on #221, #242 and #284

GPT-6 (gpt-6-astra, effort max) reviewed three merged paper-engine PRs on main ca8e37f; #243 passed.
- #221: activity-based gap recovery booked fills natively without persisting them to the durable ledger (native 3,
  durable 1); a continuing LULD pause auto-cleared after twelve minutes and let cancel_expired cancel the exit it
  protected; cash and realized P&L depended on REST-snapshot versus stream delivery order.
- #242: replay_compare scheduled a named cancel stamped before its submit and silently skipped it; cancelling the
  transport task cleared the cancel identity before the HTTP worker sent the DELETE.
- #284: a clean exporter stop released the port before deregistering, so it could remove a replacement exporter's
  target; configure.py's initialization could overwrite a concurrent registration; malformed nested labels passed
  validation and would make Prometheus reject the whole target list.

Each fix was implemented test-first by a GPT-6 implementer in its own worktree; every new regression failed before
its fix. Coordinator integration: source-hashes.json and evidence pins refreshed; the 20260923g sim-paper-compare
receipt replayed from its retained pages with the new code (identical apart from run-local fields and runner_sha256,
updated as in #242); sim-capacity's frozen replay_compare source re-pinned with a dated limitation (AST check: the
8 definitions sim-capacity uses are unchanged).

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

* Paper engine repair round: persist recovered fills through the transport's real observation hook

Independent verification of cad7faf found #221 finding 1 unresolved in production: reconcile_fills persisted recovered
fills only through a port.controller attribute that no real Alpaca port has (AlpacaPaperTransport and the native-faults
FaultTransport are wired with sink_observation=controller.observe); only the test-only SimulatedPort carried it.
- native_adapter.py: persist through the port's sink_observation (the hook the transport already uses for stream rows),
  after validating that the recovered activities tile the fill contiguously (fail closed otherwise).
- simulation.py: SimulatedPort exposes the same sink_observation hook, so tests and production share one path.
- Tests: a real-shaped port (no .controller) regression that failed on cad7faf with Decimal('1') != Decimal('3'), restart
  persistence, incomplete tiling; the two accounting-replay guards in safety.py; fast build_decisions-level tests for
  the cancel-before-submit refusal and its equality boundary.
Implemented by a GPT-6 implementer test-first; 514 tests across the five affected modules pass.

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

---------

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Sep 26, 2026
…ealized-loss delivery-order major; follow-up to #350) (#355)

* Paper engine: fix the retroactive cross-family review findings on #221, #242 and #284

GPT-6 (gpt-6-astra, effort max) reviewed three merged paper-engine PRs on main ca8e37f; #243 passed.
- #221: activity-based gap recovery booked fills natively without persisting them to the durable ledger (native 3,
  durable 1); a continuing LULD pause auto-cleared after twelve minutes and let cancel_expired cancel the exit it
  protected; cash and realized P&L depended on REST-snapshot versus stream delivery order.
- #242: replay_compare scheduled a named cancel stamped before its submit and silently skipped it; cancelling the
  transport task cleared the cancel identity before the HTTP worker sent the DELETE.
- #284: a clean exporter stop released the port before deregistering, so it could remove a replacement exporter's
  target; configure.py's initialization could overwrite a concurrent registration; malformed nested labels passed
  validation and would make Prometheus reject the whole target list.

Each fix was implemented test-first by a GPT-6 implementer in its own worktree; every new regression failed before
its fix. Coordinator integration: source-hashes.json and evidence pins refreshed; the 20260923g sim-paper-compare
receipt replayed from its retained pages with the new code (identical apart from run-local fields and runner_sha256,
updated as in #242); sim-capacity's frozen replay_compare source re-pinned with a dated limitation (AST check: the
8 definitions sim-capacity uses are unchanged).

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

* Paper engine repair round: persist recovered fills through the transport's real observation hook

Independent verification of cad7faf found #221 finding 1 unresolved in production: reconcile_fills persisted recovered
fills only through a port.controller attribute that no real Alpaca port has (AlpacaPaperTransport and the native-faults
FaultTransport are wired with sink_observation=controller.observe); only the test-only SimulatedPort carried it.
- native_adapter.py: persist through the port's sink_observation (the hook the transport already uses for stream rows),
  after validating that the recovered activities tile the fill contiguously (fail closed otherwise).
- simulation.py: SimulatedPort exposes the same sink_observation hook, so tests and production share one path.
- Tests: a real-shaped port (no .controller) regression that failed on cad7faf with Decimal('1') != Decimal('3'), restart
  persistence, incomplete tiling; the two accounting-replay guards in safety.py; fast build_decisions-level tests for
  the cancel-before-submit refusal and its equality boundary.
Implemented by a GPT-6 implementer test-first; 514 tests across the five affected modules pass.

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

* Paper engine: book realized P&L and gross loss per exact execution in accounting replay

The accounting replay summed a status delta's executions into one notional
before computing P&L, so a sell filled at a gain and a loss inside one delta
netted the loss away, and the result depended on whether the stream or the
REST snapshot arrived first. Replay now books each execution that tiles the
delta separately (QuantConnect/Lean 985ef30: SecurityPortfolioModel.cs per-fill
closing P&L; TradeStatistics.cs accumulates losses separately), falling back to
the delta notional only while executions are incomplete.

Two regressions: mixed-profit gross loss across stream-first, REST-first,
duplicate and restart deliveries, and basis preservation at each booking.
With the fix reverted, 4 cases fail (rest_first, duplicates, restart, basis).
Paper suites (safety, runner, native, transport): 407 tests OK.
Implemented by GPT-6 (gpt-6-astra, effort max) test-first; verified by the
coordinator. Pins: source-hashes.json safety.py and manifests/evidence.json.

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

* Paper engine repair round (#355 review): replay executions in execution order, versioned marker

Independent review of c81a3d5 (needs_changes):
- Finding 1 (major): the replay booked fills in observation order. Stream fills
  now carry Alpaca's own trade_updates timestamp (event_at_ns; FILL recovery
  forwards transaction_time_ns), the ledger keeps it in a new optional
  executions.at_ns column (added on open for existing ledgers), and replay
  fills each symbol's timed execution slots in execution order, tie-broken by
  event id then cumulative quantity. Adoptions, untimed executions and
  uncovered provisional bookings keep their journal positions.
- Finding 3: the marker is execution_accounting:v2:<client_id>, so ledgers
  replayed under the observation-order rule re-derive once.
- Finding 4: a test for an uncovered REST-only booking beside another order's
  late executions.
- Finding 2 (adopted open orders) is covered by #356's startup persistence.

The A/B/C case gives cash 70, realized -15, gross loss 25, and 1 share at 115
under stream, REST-ahead, recovered, late-buy and nanosecond deliveries.
Reverting the three production files fails 10 cases (4 new tests, 2 strengthened).
Paper suites: 411 OK; trading gates 26 OK; validate.py passed.
Implemented by GPT-6 (gpt-6-astra, effort max) test-first; verified by the
coordinator. Pins: source-hashes.json (safety, transport, native_adapter) and
manifests/evidence.json.

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

* Re-pin the changed paper test files in adaptive-paper/source-hashes.json

source-hashes.json also pins test files; CI's
test_adaptive_paper_source_hashes_manifest caught the stale entries. Pins only,
no code change: the changed tests' entries and their manifests/evidence.json
registration.

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

* Reduce #355 to the reviewed-safe subset; defer execution-order replay

The re-check of cc6d7cb found two new majors in the execution-order replay:
- Moving an earlier-executed fill into another order's slot can make an
  uncovered REST-only sell go short. The replay then raises, the fill rolls
  back and the transport freezes, although the real history is consistent.
- Removing the early exit let that failure block fills in every symbol and
  added a full replay to every fill.
Under the bounded review loop (one review, one repair, then land with
residuals), this commit lands the parts both reviews confirmed.

It keeps:
- c81a3d5's per-execution P&L and gross loss within a delta;
- the versioned execution_accounting:v2 marker (F3);
- the uncovered-fallback test (F4), with its oracle stated for
  observation-order replay.

It reverts:
- the slot-substitution replay and the early-exit removal;
- the executions.at_ns column, which also avoids the unversioned schema
  change (re-check F3);
- the stream and recovery timestamp plumbing.

Execution-time ordering is a recorded follow-up, with the reviewer's
constraints: never reorder into a short (fall back to observation order), a
targeted replay trigger, and a schema version bump.

Revert checks: the v1 marker fails the F3 test; skipping the uncovered
fallback errors the F4 test. Paper suites: 409 OK; source-hashes and trading
gates: 33 OK; validate.py passed. Pins: safety.py and its test file only.

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

---------

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:shared Touches files owned by both lanes; needs both lanes' acknowledgement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant