feat(agent-004): implement full Validator Monitor workflow mirroring AGENT-002/003#81
Conversation
Mirrors the AGENT-002 / AGENT-003 structure to give AGENT-004 Validator Monitor the same standalone TypeScript implementation: - `agent-004-validator-monitor/` — standalone Node.js process - `src/index.ts` — banner, cycle runner, polling vs single-run mode - `src/config.ts` — config + thresholds mirrored from the character - `src/types.ts` — staking / slashing / gov types + per-workflow shapes - `src/ledger.ts` — LCD client for staking, slashing, gov endpoints - `src/store.ts` — SQLite state (token snapshots, commission history, scorecards, decentralization snapshots, workflow executions) - `src/ooda.ts` — generic OODA executor (same shape as agent-002/003) - `src/monitor.ts` — Claude narrative layer, one function per workflow - `src/output.ts` — console + optional Discord webhook dispatcher - `src/workflows/performance-tracking.ts` — WF-VM-01 - `src/workflows/delegation-flow-analysis.ts` — WF-VM-02 - `src/workflows/decentralization-monitor.ts` — WF-VM-03 Scoring (M014): - Uptime component: signed_blocks_window − missed_blocks_counter, weighted at config.validator.scoreWeightUptime (default 400) - Governance component: votes cast / recent finalized proposals, weighted at config.validator.scoreWeightGovernance (default 350) - Stability component: full weight minus penalties for jailing (100) and commission changes in the trailing window (40 each), floored at 0, weighted at config.validator.scoreWeightStability (default 250) - Composite = sum, 0..1000, PoA eligible when >= 800 Decentralization (WF-VM-03): - Nakamoto coefficient uses the 33.4% halt-threshold convention - Gini index uses the textbook formula on token amounts normalized to uregen - Health classification from Nakamoto floor (<=5 CRITICAL, <=8 WARNING), Gini ceiling (>=0.65 WARNING), and single-validator concentration (>=33% CRITICAL, >=20% WARNING) Delegation flows (WF-VM-02): - Snapshot each validator's `tokens` field every cycle - Derive flows from the delta against the previous snapshot - Whale-sized movements (>= 100,000 REGEN by default) tagged in the summary and raise the alert level Determinism: all scoring, Nakamoto, Gini, health, and whale detection are computed in plain TypeScript. Claude is only used for the narrative layer. Keeps the agent cheap, reproducible, and auditable — and makes the hard parts trivially unit-testable in a follow-up PR. Deliberate MVP proxies (documented in the agent README): 1. Token-delta as the delegation source for WF-VM-02 until a real MsgDelegate / MsgUndelegate / MsgRedelegate tx-stream client lands. 2. Governance participation currently scores 0 for every validator because the operator→delegator bech32 conversion is deferred to a follow-up PR. Relative composites still surface real differences in uptime and stability without asymmetric penalty. 3. MVP signing-info join on the raw consensus key — falls back to assuming 100% uptime when no match is found, which under-counts real issues rather than smearing a healthy validator. CI: the new agent is added to the `agents` job so `npx tsc --noEmit` runs against it on every PR, matching the existing agent-002-governance-analyst wiring. - Lands in: `agent-004-validator-monitor/`, `.github/workflows/ci.yml` - Changes: new standalone AGENT-004 process with 3 workflows (WF-VM-01/02/03) - Validate: `cd agent-004-validator-monitor && npm ci && npx tsc --noEmit` Refs phase-2/2.2-agentic-workflows.md §WF-VM-01, §WF-VM-02, §WF-VM-03 Refs agents/packages/agents/src/characters/validator-monitor.ts (regen-network#64)
There was a problem hiding this comment.
Code Review
This pull request introduces AGENT-004, a standalone Node.js agent designed to monitor Regen Network validators. It implements three OODA-based workflows for performance tracking, delegation flow analysis, and decentralization monitoring, utilizing a local SQLite store and Claude for narrative reporting. The review feedback identifies a critical bug where mismatched consensus key formats prevent functional uptime scoring, a logic error in the retrieval of historical decentralization snapshots, and precision loss in Gini index calculations. Additionally, recommendations were made to improve the robustness of the main polling loop and the efficiency of the performance tracking logic.
| const signing = signingByConsAddrLike.get( | ||
| v.consensus_pubkey?.key || "" | ||
| ); |
There was a problem hiding this comment.
The join between signingByConsAddrLike and the validator's consensus key will always fail in the current implementation. v.consensus_pubkey.key is a base64-encoded public key, while info.address (from the slashing signing info endpoint) is a bech32-encoded consensus address (e.g., regenvalcons1...). Because these formats never match, the agent will always fall back to assuming 0 missed blocks for every validator, rendering the uptime monitoring and scoring non-functional.
| const interval = setInterval(() => { | ||
| runCycle(ledger).catch((err) => | ||
| console.error(`Cycle failed:`, err) | ||
| ); | ||
| }, config.pollIntervalMs); |
There was a problem hiding this comment.
Using setInterval for the main loop can lead to overlapping execution cycles if a cycle takes longer than pollIntervalMs (e.g., due to network latency or slow LLM responses). This can cause race conditions or database locks. It is safer to use a recursive setTimeout pattern to ensure that a new cycle only starts after the previous one has completed.
| `SELECT nakamoto, gini, top10_pct, health, snapshot, captured_at | ||
| FROM decentralization_snapshots ORDER BY id DESC LIMIT 1 OFFSET 1` | ||
| ) | ||
| .get() as |
There was a problem hiding this comment.
The OFFSET 1 in this query causes it to skip the most recent snapshot. Since this method is called in decentralization-monitor.ts before the current cycle's snapshot is saved, it will return the snapshot from two cycles ago instead of the immediate previous one. This skews the trend analysis in the narrative report.
FROM decentralization_snapshots ORDER BY id DESC LIMIT 1 OFFSET 0`| const total = tokensBig.reduce((acc, t) => acc + t, 0n); | ||
| const sortedDesc = [...tokensBig].sort((a, b) => (a > b ? -1 : a < b ? 1 : 0)); | ||
|
|
||
| const tokensNum = active.map((v) => Number(BigInt(v.tokens || "0") / 1_000_000n)); |
There was a problem hiding this comment.
Converting token counts to numbers using integer division (/ 1_000_000n) results in a significant loss of precision, especially for smaller validators. Since the Gini index is sensitive to the distribution of these values, this loss of precision can lead to an inaccurate decentralization score. It is better to perform the division using floating-point numbers after converting to Number.
| const tokensNum = active.map((v) => Number(BigInt(v.tokens || "0") / 1_000_000n)); | |
| const tokensNum = active.map((v) => Number(BigInt(v.tokens || "0")) / 1_000_000); |
| const sinceIso = new Date( | ||
| Date.now() - config.validator.uptimeTrailingDays * 86_400_000 | ||
| ).toISOString(); |
The AGENT-001 Registry Reviewer standalone TypeScript implementation has been in the repo since PR regen-network#64 but was never wired into the CI `Agents` job. Only `agents/` (the ElizaOS character pack) and `agent-002-governance-analyst/` run `npx tsc --noEmit` on every PR. `agent-001-registry-reviewer/` has its own `package.json`, `tsconfig.json`, and a full workflow implementation (reviewer, ooda, 3 workflows, ledger client, store, types). It compiles cleanly on Node 22 locally — this PR adds it to the CI matrix so regressions are caught on every PR. After PRs regen-network#80 and regen-network#81 (which added agent-003 and agent-004 to CI), this PR closes the gap for the only remaining unwired standalone agent: agent-001. The full CI Agents job now typechecks four standalone agent processes: agents/ (ElizaOS character pack) agent-001-registry-reviewer/ (this PR) agent-002-governance-analyst/ (already wired) agent-003-market-monitor/ (added in PR regen-network#80) agent-004-validator-monitor/ (added in PR regen-network#81) ## Validation $ cd agent-001-registry-reviewer && npm ci && npx tsc --noEmit (exit 0) Same commands as the CI step. - Lands in: `.github/workflows/ci.yml` - Changes: add agent-001-registry-reviewer npm ci + typecheck steps - Validate: `cd agent-001-registry-reviewer && npm ci && npx tsc --noEmit`
Sibling PR to the AGENT-003 unit tests (regen-network#99) and a follow-up to PR regen-network#81, which promised the unit tests as a "separate test-only PR so this PR stays a single-concern 'add the agent' change." Adds 33 unit tests across 2 test files covering every deterministic helper in the AGENT-004 workflows. The helpers are the core of the decentralization analysis surface — if they drift silently, the validator monitor produces misleading alerts or misses real concentration attacks. ## Changes ### Helper exports Five previously module-private helpers are now exported so the test files can import them: decentralization-monitor.ts: nakamotoCoefficient, giniIndex, topNSharePct, classifyHealth delegation-flow-analysis.ts: absBig The export is the only production-code change — no behavior change, no API rename. Module consumers are unchanged. ### Test files src/workflows/decentralization-monitor.test.ts (28 tests) nakamotoCoefficient — 8 tests - empty input / zero total returns 0 - single validator with entire stake → 1 - top validator > 33.4% → 1 - top validator exactly 33.4% (334/1000) → 2 (pins the STRICT `> threshold` predicate — a refactor that changes > to >= would silently produce Nakamoto = 1 here) - top two combined clear threshold → 2 - ten equal validators → 4 - degenerate case when total > sum of list giniIndex — 7 tests - empty / single-element → 0 - perfect equality → 0 - unequal distribution > 0 - maximally unequal (one holds everything) → approaches 1 - all-zeros → 0 (cumulative guard) - monotonicity: more inequality → higher Gini topNSharePct — 6 tests - zero total → 0 - top 1, top 3 cumulative share - n > array length → 100% - n = array length → 100% - two-decimal precision classifyHealth — 7 tests - HEALTHY baseline - CRITICAL on Nakamoto floor - CRITICAL on single-validator concentration - WARNING on Nakamoto warning floor - WARNING on Gini ceiling - WARNING on single-validator warning concentration - CRITICAL wins over WARNING when thresholds overlap src/workflows/delegation-flow-analysis.test.ts (5 tests) absBig — 5 tests - zero - positive inputs - negative inputs - values beyond Number.MAX_SAFE_INTEGER (2^53 + 1) — critical because AGENT-004 works in uregen and 221M REGEN is 2.21e14 uregen, close to the unsafe-integer boundary - idempotence ### Vitest setup Same structure as AGENT-003's test PR (regen-network#99): vitest.config.ts — standard config, node env package.json — adds "test" / "test:watch" + vitest ^2.1.0 tsconfig.json — excludes *.test.ts from prod typecheck .gitignore — adds *.db-shm and *.db-wal ## Validation $ cd agent-004-validator-monitor && npm test Test Files 2 passed (2) Tests 33 passed (33) $ cd agent-004-validator-monitor && npx tsc --noEmit (exit 0) Note: the tests import decentralization-monitor.ts, which in turn imports store.ts at the top level. The store constructor opens a SQLite database on import. If a prior test run left stale WAL lock files on disk, the next run fails with "database is locked". The .gitignore update prevents those lock files from landing in a PR; developers running tests locally may need to rm -f agent-004.db-shm agent-004.db-wal once if they hit the lock. ## Scope Does NOT touch the OODA loops, the Claude narrative layer, the LCD client, the SQLite store, or any output formatting. Tests cover pure functions only. - Lands in: `agent-004-validator-monitor/` - Changes: 33 unit tests + vitest setup + 5 helper exports - Validate: `cd agent-004-validator-monitor && npm test` ## PR relationship Based on PR regen-network#81's branch. If regen-network#81 merges first, this PR rebases cleanly. Sibling PR to regen-network#99 (AGENT-003 unit tests) — the two follow an identical structure and review together better than separately.
Replaces the token-delta MVP proxy with a real staking tx-search client that reads recent MsgDelegate, MsgUndelegate, and MsgBeginRedelegate events from the Cosmos LCD. Closes the follow-up documented in PR regen-network#81's design-decision regen-network#2. ## What changes in the workflow The observe phase no longer snapshots `validator.tokens` or consults the previous snapshot. Instead: const events = await ledger.getRecentDelegationTxs(200); The orient phase aggregates events per-validator via a new pure function `aggregateEventsToFlows`, which handles three rules: - delegate → inflow to event.validator - undelegate → outflow from event.validator - redelegate → outflow from event.sourceValidator, inflow to event.validator (destination) `summarizeFlows` (also new and exported for tests) derives the totals, whale count, top inflow, and top outflow from the flow list. Neither function touches the store — the old per-cycle token snapshot is no longer needed. The monikers for the narrative layer are still backfilled from a single `ledger.getValidators()` call inside the orient phase. ## What changes in the ledger client New methods on LedgerClient: - `getRecentDelegationTxs(limit)` — queries the LCD tx-search endpoint once per staking message type (three type URLs total) and flattens the results into a single DelegationEvent list. Per-type failures are isolated: if the MsgUndelegate query fails for any reason, the MsgDelegate and MsgBeginRedelegate results still come through. - `parseDelegationEventsFromTx(tx)` — public pure function. Walks events at both `logs[].events[]` and top-level `tx.events[]` positions for cross-SDK compatibility. Matches three Cosmos SDK event types: `delegate`, `unbond`, and `redelegate`. Extracts the delegator address from the positionally-corresponding `message` event sender. - New helper `parseCoinAmount(raw)` extracts the numeric prefix from Cosmos coin-amount strings like "1000uregen", returning the numeric part as a string for BigInt-safe downstream consumption. ## New types - `DelegationEvent` — a single on-chain staking event with txHash, eventType, delegator, validator, sourceValidator (only set for redelegate), amountUregen, and occurredAt. The DelegationFlow type is preserved for backward compatibility with the narrative layer. ## New tests — 22 total (55 total across 3 files, up from 33 in regen-network#100) ### src/ledger.test.ts (9 new tests) - empty tx - MsgDelegate extraction with sender from message event - MsgUndelegate extraction via the `unbond` event type - MsgBeginRedelegate with source + destination validators - batched: 3 staking events in one tx with positional sender matching - missing validator attribute ignored - malformed amount attribute ignored - coin-amount format "<uint>uregen" parsed correctly - events read from tx.events[] alongside logs[].events[] ### src/workflows/delegation-flow-analysis.test.ts (13 new tests, on top of the existing 5 for absBig) - aggregateEventsToFlows: - empty - single delegate → inflow - single undelegate → outflow - redelegate → two flows (source + destination) - net delegate + undelegate on same validator - zero net delta skipped - whale threshold tagging - zero/negative amount skipped - non-numeric amount skipped - summarizeFlows: - zero totals on empty - inflow / outflow / net sum correctness - top inflow + top outflow identification - whale count separate from total ## What this unlocks The old MVP proxy couldn't: - Distinguish delegate from undelegate from redelegate. A validator losing 100K stake could be a pure outflow (undelegate) or a redelegate to a different validator, but the proxy saw the same delta number either way. - Attribute flows to a specific delegator address. - Capture intra-cycle movements that net to zero (A delegates, B undelegates same amount within a minute — the old proxy reported "no change" and missed both events). - Produce a reliable audit trail linking back to real tx hashes. The new implementation fixes all four. ## Scope Does NOT touch WF-VM-01 (performance tracking) or WF-VM-03 (decentralization monitor). The bech32 operator→delegator conversion for governance participation scoring is a separate follow-up (the m014 governance score is still MVP-zero after this PR). - Lands in: `agent-004-validator-monitor/` - Changes: new tx-search client + new DelegationEvent type + rewritten WF-VM-02 observe+orient phases + 22 new tests - Validate: `cd agent-004-validator-monitor && npm test && npx tsc --noEmit` ## PR relationship Based on PR regen-network#100 (AGENT-004 unit tests) which is based on PR regen-network#81 (AGENT-004 initial implementation). Sibling to PR regen-network#103 (AGENT-003 MsgRetire tx-stream). The two real-tx-stream PRs close the MVP-proxy column for both market-monitor and validator-monitor in the same session. Refs `phase-2/2.2-agentic-workflows.md` §WF-VM-02 Refs PR regen-network#81's design decision regen-network#2 (MVP token-delta proxy → real tx-stream follow-up)
…e scoring
Closes the last MVP proxy in AGENT-004 — the governance score in
WF-VM-01 was hardcoded to zero because the operator→delegator
bech32 conversion required to query per-validator votes was
deferred to this follow-up. This PR delivers it.
## What changes
### Bech32 converter
New exported pure function `operatorToAccountBech32`:
regenvaloper1abc... → regen1abc...
It decodes the operator bech32, verifies the HRP ends in
"valoper", strips that suffix to get the delegator HRP, and
re-encodes the same word payload. Returns null on any invalid
input — not a bech32, HRP not ending in valoper, empty delegator
prefix, or bad checksum. The null-instead-of-throw shape lets
the observe phase skip broken validators without crashing the
whole cycle.
The underlying bech32 library is `bech32@^2.0.0` — a zero-
runtime-dependency npm package that implements the reference
encoding from BIP-0173. Added to `package.json` as the only new
runtime dependency.
### Observe phase rewired
For each validator in the set, the observe phase now:
1. Converts operator → delegator bech32.
2. For each recent finalized proposal, calls
`ledger.getVoteForVoter(proposalId, delegatorAddress)`.
3. Counts the number of successful vote lookups.
Validators whose operator address does not convert cleanly get
a zero count — no crash, no asymmetric penalty relative to
validators that DID vote zero times on actual proposals.
The fan-out is O(validators × proposals) per cycle — up to
~75 × 20 = 1500 LCD requests on mainnet. Both loops run in
parallel with Promise.all so wall-clock time is bounded by the
slowest request, not the total count. A future optimization can
batch the queries by fetching `/proposals/{id}/votes` once per
proposal and indexing locally.
### Orient phase uses real numbers
No math changes — the existing scoring formula already read
`obs.votesCastByOperator`. Previously the map was empty; now
it's populated. The resulting composite score for validators
that DID vote reflects real governance participation, and the
PoA eligibility calculation (`composite >= 800`) can now
meaningfully distinguish validators that participate in
governance from validators that only sign blocks.
## New tests — 7 total
**src/workflows/performance-tracking.test.ts** (new test file)
All seven tests use real `bech32.encode` to build test inputs
rather than hand-rolled fixture strings. This way, any change
in the bech32 library's output format would be caught
automatically instead of silently drifting past the tests.
- converts regenvaloper → regen
- converts cosmosvaloper → cosmos (chain-agnostic)
- round-trip decode produces identical 20-byte payload
- returns null for non-bech32 input
- returns null for delegator bech32 (no valoper suffix)
- returns null when HRP is exactly "valoper" (empty prefix after strip)
- returns null for a bech32 address with a bad checksum
Full test suite: 62 passed across 4 test files (up from 55 in regen-network#104).
## What this unlocks
With real governance scores in the composite, the M014
eligibility decision now reflects the full SPEC methodology:
- Uptime (400/1000): signed blocks in trailing window
- Governance (350/1000): % of recent finalized proposals voted on
- Stability (250/1000): jailing and commission penalty
A validator that perfectly signs blocks but never votes now
scores ~650 and does NOT clear the 800 PoA threshold — as the
SPEC intends. Previously both "perfect signer, zero governance"
and "perfect signer, perfect governance" produced the same
score of 650 because governance was hardcoded zero. The MVP
hid a real distinction; this PR surfaces it.
## Scope
Does NOT touch WF-VM-02 (delegation flow — addressed in regen-network#104)
or WF-VM-03 (decentralization). Does NOT implement per-proposal
vote batching (noted as a future optimization). Does NOT fix
the signing-info join which is still MVP-positional — a future
PR can address that by computing the valcons bech32 from the
consensus pubkey.
- Lands in: `agent-004-validator-monitor/`
- Changes: new bech32 converter + real vote fetching + 7 new tests
- Validate: `cd agent-004-validator-monitor && npm test && npx tsc --noEmit`
## PR relationship
Based on PR regen-network#104 (AGENT-004 real delegation tx-stream). Together
with regen-network#103 (AGENT-003 real MsgRetire tx-stream) and regen-network#104, this
PR closes every MVP-proxy column in both agent implementations.
The only remaining known limitation in the AGENT-004 WF-VM-01
path is the signing-info join, which is an orthogonal fix.
Refs `mechanisms/m014-authority-validator-governance/SPEC.md` §5.3
Refs `phase-2/2.2-agentic-workflows.md` §WF-VM-01
Refs PR regen-network#81's design decision regen-network#3 (MVP-zero governance → real scoring follow-up)
…T, loop, commission Addresses Gemini review feedback on PR regen-network#81: WF-VM-01 (performance-tracking): * Derive the validator's regenvalcons1… consensus address from its consensus pubkey (SHA256 of pubkey bytes → first 20 bytes → bech32 under the consensus HRP) and use that as the signingByConsAddrLike lookup key. The old code was joining a base64 pubkey string against a bech32 address, so the lookup always missed and every validator reported 0 missed blocks — uptime scored 100% across the board. * Hoist the trailing-window `sinceIso` out of the per-validator loop so we do not recompute the same Date arithmetic N times per cycle. * Drop the dead `operatorToAccountBech32` stub and use the real `operatorToDelegator` helper from the new bech32 module. WF-VM-03 (decentralization-monitor): * Gini index now converts the full uregen value to Number without pre-dividing by 1_000_000n. Integer division floored every validator with less than 1 REGEN to zero and discarded fractional REGEN for larger stakes, both of which distort the Gini. uregen fits inside Number.MAX_SAFE_INTEGER safely, so no precision is lost by keeping the full value. Store: * countCommissionChangesSince now checks whether a baseline row exists *before* the window and counts the in-window rows accordingly. The old `cnt - 1` path dropped a real commission change whenever the baseline read fell outside the window. * getLatestDecentralizationSnapshot uses `LIMIT 1` (no OFFSET). The caller runs in the `orient` phase before the current cycle's snapshot is written, so the newest row is the actual previous cycle's snapshot. `OFFSET 1` was skipping it and either comparing against the cycle before last or returning null on the second run. * Store constructor accepts an optional dbPath (defaulting to the on-disk DB file) so unit tests can pass `:memory:` without clobbering the shared DB or hitting the "database is locked" failure mode. New src/bech32.ts module: * consensusPubkeyToConsAddress, operatorToDelegator, delegatorToOperator built on the `bech32` npm package. Centralized so WF-VM-01 and the forthcoming WF-VM-02 real tx-stream (PR regen-network#104) share one implementation. * Added `bech32: ^2.0.0` dependency to agent-004/package.json. Main loop: * setInterval → recursive setTimeout so a slow cycle cannot overlap with the next tick. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…T, loop, commission Addresses Gemini review feedback on PR regen-network#81: WF-VM-01 (performance-tracking): * Derive the validator's regenvalcons1… consensus address from its consensus pubkey (SHA256 of pubkey bytes → first 20 bytes → bech32 under the consensus HRP) and use that as the signingByConsAddrLike lookup key. The old code was joining a base64 pubkey string against a bech32 address, so the lookup always missed and every validator reported 0 missed blocks — uptime scored 100% across the board. * Hoist the trailing-window `sinceIso` out of the per-validator loop so we do not recompute the same Date arithmetic N times per cycle. * Drop the dead `operatorToAccountBech32` stub and use the real `operatorToDelegator` helper from the new bech32 module. WF-VM-03 (decentralization-monitor): * Gini index now converts the full uregen value to Number without pre-dividing by 1_000_000n. Integer division floored every validator with less than 1 REGEN to zero and discarded fractional REGEN for larger stakes, both of which distort the Gini. uregen fits inside Number.MAX_SAFE_INTEGER safely, so no precision is lost by keeping the full value. Store: * countCommissionChangesSince now checks whether a baseline row exists *before* the window and counts the in-window rows accordingly. The old `cnt - 1` path dropped a real commission change whenever the baseline read fell outside the window. * getLatestDecentralizationSnapshot uses `LIMIT 1` (no OFFSET). The caller runs in the `orient` phase before the current cycle's snapshot is written, so the newest row is the actual previous cycle's snapshot. `OFFSET 1` was skipping it and either comparing against the cycle before last or returning null on the second run. * Store constructor accepts an optional dbPath (defaulting to the on-disk DB file) so unit tests can pass `:memory:` without clobbering the shared DB or hitting the "database is locked" failure mode. New src/bech32.ts module: * consensusPubkeyToConsAddress, operatorToDelegator, delegatorToOperator built on the `bech32` npm package. Centralized so WF-VM-01 and the forthcoming WF-VM-02 real tx-stream (PR regen-network#104) share one implementation. * Added `bech32: ^2.0.0` dependency to agent-004/package.json. Main loop: * setInterval → recursive setTimeout so a slow cycle cannot overlap with the next tick. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…orkflow fixes Addresses Gemini review feedback on PR regen-network#100: * The Store singleton is now constructed lazily via a Proxy, so importing a workflow file for its exported helpers (which is what decentralization-monitor.test.ts and delegation-flow-analysis.test.ts do) no longer opens the SQLite DB as a side effect. That eliminates the "database is locked" failure mode when the test suites run in parallel. * The Store class already accepts an optional `dbPath` (see the PR regen-network#81 fix that this commit cherry-picks on top) so a future test can construct its own `new Store(":memory:")` without touching the shared DB file. * Also cherry-picks the PR regen-network#81 Gemini-review fixes so this branch is self-consistent: bech32-derived consensus address for uptime lookup, Gini precision via full uregen Number, countCommissionChangesSince baseline-aware logic, OFFSET 1 → OFFSET 0 in getLatestDecentralizationSnapshot, recursive setTimeout main loop. Full vitest suite: 33/33 passing. Typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…T, loop, commission Addresses Gemini review feedback on PR regen-network#81: WF-VM-01 (performance-tracking): * Derive the validator's regenvalcons1… consensus address from its consensus pubkey (SHA256 of pubkey bytes → first 20 bytes → bech32 under the consensus HRP) and use that as the signingByConsAddrLike lookup key. The old code was joining a base64 pubkey string against a bech32 address, so the lookup always missed and every validator reported 0 missed blocks — uptime scored 100% across the board. * Hoist the trailing-window `sinceIso` out of the per-validator loop so we do not recompute the same Date arithmetic N times per cycle. * Drop the dead `operatorToAccountBech32` stub and use the real `operatorToDelegator` helper from the new bech32 module. WF-VM-03 (decentralization-monitor): * Gini index now converts the full uregen value to Number without pre-dividing by 1_000_000n. Integer division floored every validator with less than 1 REGEN to zero and discarded fractional REGEN for larger stakes, both of which distort the Gini. uregen fits inside Number.MAX_SAFE_INTEGER safely, so no precision is lost by keeping the full value. Store: * countCommissionChangesSince now checks whether a baseline row exists *before* the window and counts the in-window rows accordingly. The old `cnt - 1` path dropped a real commission change whenever the baseline read fell outside the window. * getLatestDecentralizationSnapshot uses `LIMIT 1` (no OFFSET). The caller runs in the `orient` phase before the current cycle's snapshot is written, so the newest row is the actual previous cycle's snapshot. `OFFSET 1` was skipping it and either comparing against the cycle before last or returning null on the second run. * Store constructor accepts an optional dbPath (defaulting to the on-disk DB file) so unit tests can pass `:memory:` without clobbering the shared DB or hitting the "database is locked" failure mode. New src/bech32.ts module: * consensusPubkeyToConsAddress, operatorToDelegator, delegatorToOperator built on the `bech32` npm package. Centralized so WF-VM-01 and the forthcoming WF-VM-02 real tx-stream (PR regen-network#104) share one implementation. * Added `bech32: ^2.0.0` dependency to agent-004/package.json. Main loop: * setInterval → recursive setTimeout so a slow cycle cannot overlap with the next tick. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses Gemini review feedback on PR regen-network#104: LedgerClient.parseDelegationEventsFromTx: * Walk `tx.logs` message-by-message so each staking event is attributed to the delegator (`sender`) of its own message. The previous implementation flattened all events, collected every `message.sender` into a positional array, and mapped the Nth staking event to the Nth sender. That mis-attributed staking events whenever a tx mixed staking with non-staking messages (e.g. `[MsgDelegate, MsgSend, MsgUndelegate]` would attribute the undelegate to the MsgSend sender). * Prefer `tx.logs[i].events[]` over a mixed `tx.logs + tx.events` merge. In modern Cosmos SDK versions `tx.events` is a flattened superset of everything in the logs, so walking both double-counts every staking event (delegation deltas ended up 2× reality). We only fall back to `tx.events` when `tx.logs` is empty (very old LCD builds). LedgerClient.getRecentDelegationTxs: * Dedupe transactions by hash across the three per-type-URL queries so a tx containing more than one of the three message types (e.g. atomic `MsgDelegate` + `MsgUndelegate`) is only parsed once. Previously every event in such a tx was aggregated multiple times, inflating the per-cycle delegation deltas. * Replace the silent `catch {}` with `console.error` so network failures are visible. Also cherry-picks the PR regen-network#81 Gemini-review fixes so this branch is self-consistent (bech32 consensus address derivation, Gini precision, OFFSET 1 → 0, commission baseline, recursive setTimeout main loop). Tests: updated the "multiple staking events in a single batched tx" case to use the realistic one-log-per-message shape, and added a new "attributes staking events through interleaved non-staking messages" case that directly pins the bug the previous positional-match code was broken by. Full vitest suite: 56/56 passing. Typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…T, loop, commission Addresses Gemini review feedback on PR regen-network#81: WF-VM-01 (performance-tracking): * Derive the validator's regenvalcons1… consensus address from its consensus pubkey (SHA256 of pubkey bytes → first 20 bytes → bech32 under the consensus HRP) and use that as the signingByConsAddrLike lookup key. The old code was joining a base64 pubkey string against a bech32 address, so the lookup always missed and every validator reported 0 missed blocks — uptime scored 100% across the board. * Hoist the trailing-window `sinceIso` out of the per-validator loop so we do not recompute the same Date arithmetic N times per cycle. * Drop the dead `operatorToAccountBech32` stub and use the real `operatorToDelegator` helper from the new bech32 module. WF-VM-03 (decentralization-monitor): * Gini index now converts the full uregen value to Number without pre-dividing by 1_000_000n. Integer division floored every validator with less than 1 REGEN to zero and discarded fractional REGEN for larger stakes, both of which distort the Gini. uregen fits inside Number.MAX_SAFE_INTEGER safely, so no precision is lost by keeping the full value. Store: * countCommissionChangesSince now checks whether a baseline row exists *before* the window and counts the in-window rows accordingly. The old `cnt - 1` path dropped a real commission change whenever the baseline read fell outside the window. * getLatestDecentralizationSnapshot uses `LIMIT 1` (no OFFSET). The caller runs in the `orient` phase before the current cycle's snapshot is written, so the newest row is the actual previous cycle's snapshot. `OFFSET 1` was skipping it and either comparing against the cycle before last or returning null on the second run. * Store constructor accepts an optional dbPath (defaulting to the on-disk DB file) so unit tests can pass `:memory:` without clobbering the shared DB or hitting the "database is locked" failure mode. New src/bech32.ts module: * consensusPubkeyToConsAddress, operatorToDelegator, delegatorToOperator built on the `bech32` npm package. Centralized so WF-VM-01 and the forthcoming WF-VM-02 real tx-stream (PR regen-network#104) share one implementation. * Added `bech32: ^2.0.0` dependency to agent-004/package.json. Main loop: * setInterval → recursive setTimeout so a slow cycle cannot overlap with the next tick. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses Gemini review feedback on PR regen-network#104: LedgerClient.parseDelegationEventsFromTx: * Walk `tx.logs` message-by-message so each staking event is attributed to the delegator (`sender`) of its own message. The previous implementation flattened all events, collected every `message.sender` into a positional array, and mapped the Nth staking event to the Nth sender. That mis-attributed staking events whenever a tx mixed staking with non-staking messages (e.g. `[MsgDelegate, MsgSend, MsgUndelegate]` would attribute the undelegate to the MsgSend sender). * Prefer `tx.logs[i].events[]` over a mixed `tx.logs + tx.events` merge. In modern Cosmos SDK versions `tx.events` is a flattened superset of everything in the logs, so walking both double-counts every staking event (delegation deltas ended up 2× reality). We only fall back to `tx.events` when `tx.logs` is empty (very old LCD builds). LedgerClient.getRecentDelegationTxs: * Dedupe transactions by hash across the three per-type-URL queries so a tx containing more than one of the three message types (e.g. atomic `MsgDelegate` + `MsgUndelegate`) is only parsed once. Previously every event in such a tx was aggregated multiple times, inflating the per-cycle delegation deltas. * Replace the silent `catch {}` with `console.error` so network failures are visible. Also cherry-picks the PR regen-network#81 Gemini-review fixes so this branch is self-consistent (bech32 consensus address derivation, Gini precision, OFFSET 1 → 0, commission baseline, recursive setTimeout main loop). Tests: updated the "multiple staking events in a single batched tx" case to use the realistic one-log-per-message shape, and added a new "attributes staking events through interleaved non-staking messages" case that directly pins the bug the previous positional-match code was broken by. Full vitest suite: 56/56 passing. Typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses Gemini review feedback on PR regen-network#105: Governance scoring: * Replace the O(validators × proposals) per-voter fan-out with an O(proposals) per-proposal batch fetch. The old code was firing up to ~1500 LCD requests per cycle (75 validators × 20 proposals) and would reliably trip rate limits on public endpoints. We now call `/cosmos/gov/v1beta1/proposals/{id}/votes` once per proposal, paginate through the result, and index the voters locally — cost is 20 requests instead of 1500. * Add a paginated `ledger.getVotesForProposal` with a 25-page safety cap (500 votes × 25 = 12.5k voter headroom). Bech32 consolidation: * The inline `operatorToAccountBech32` helper was moved into `src/bech32.ts` in the PR regen-network#81 fix (this branch cherry-picks that fix) and renamed `operatorToDelegator`. Both the workflow and the existing unit tests now import from the shared module. * `operatorToDelegator` preserves the same guarantees the inline version had (strict HRP suffix check, empty-prefix guard, chain- agnostic HRP handling) so the PR regen-network#105 test suite still passes against the centralized implementation. * Also cherry-picks the PR regen-network#81 review fixes (bech32 consensus address derivation for signing info lookup, Gini precision fix, OFFSET 1 → 0, commission baseline logic, recursive setTimeout main loop) and the PR regen-network#104 review fixes (log-scoped delegator attribution, tx.events vs logs dedup, tx-hash dedup across staking type queries) so this branch is self-consistent when its ancestors rebase. Full vitest suite: 63/63 passing. Typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Superseded by #105 — the bech32 + real M014 governance scoring branch includes the full agent-004 workflow from this PR plus the real delegation tx-stream (#104) on top, and all gemini-review fixes from this PR are already present on #105 (commit a903a58 is the equivalent of this PR's bcb173f). Closing as superseded. |
#96) The AGENT-001 Registry Reviewer standalone TypeScript implementation has been in the repo since PR #64 but was never wired into the CI `Agents` job. Only `agents/` (the ElizaOS character pack) and `agent-002-governance-analyst/` run `npx tsc --noEmit` on every PR. `agent-001-registry-reviewer/` has its own `package.json`, `tsconfig.json`, and a full workflow implementation (reviewer, ooda, 3 workflows, ledger client, store, types). It compiles cleanly on Node 22 locally — this PR adds it to the CI matrix so regressions are caught on every PR. After PRs #80 and #81 (which added agent-003 and agent-004 to CI), this PR closes the gap for the only remaining unwired standalone agent: agent-001. The full CI Agents job now typechecks four standalone agent processes: agents/ (ElizaOS character pack) agent-001-registry-reviewer/ (this PR) agent-002-governance-analyst/ (already wired) agent-003-market-monitor/ (added in PR #80) agent-004-validator-monitor/ (added in PR #81) ## Validation $ cd agent-001-registry-reviewer && npm ci && npx tsc --noEmit (exit 0) Same commands as the CI step. - Lands in: `.github/workflows/ci.yml` - Changes: add agent-001-registry-reviewer npm ci + typecheck steps - Validate: `cd agent-001-registry-reviewer && npm ci && npx tsc --noEmit`
…orkflow fixes Addresses Gemini review feedback on PR regen-network#100: * The Store singleton is now constructed lazily via a Proxy, so importing a workflow file for its exported helpers (which is what decentralization-monitor.test.ts and delegation-flow-analysis.test.ts do) no longer opens the SQLite DB as a side effect. That eliminates the "database is locked" failure mode when the test suites run in parallel. * The Store class already accepts an optional `dbPath` (see the PR regen-network#81 fix that this commit cherry-picks on top) so a future test can construct its own `new Store(":memory:")` without touching the shared DB file. * Also cherry-picks the PR regen-network#81 Gemini-review fixes so this branch is self-consistent: bech32-derived consensus address for uptime lookup, Gini precision via full uregen Number, countCommissionChangesSince baseline-aware logic, OFFSET 1 → OFFSET 0 in getLatestDecentralizationSnapshot, recursive setTimeout main loop. Full vitest suite: 33/33 passing. Typecheck clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Gives AGENT-004 Validator Monitor the same full standalone TypeScript implementation that AGENT-002 Governance Analyst and AGENT-003 Market Monitor already have. Character definition was merged in #64; this PR adds the workflow code that runs against Regen Ledger.
Structure — mirrors AGENT-002/003
Scoring methodology (M014)
Composite score is 0-1000, computed deterministically in `performance-tracking.ts`:
PoA eligibility: composite >= `config.validator.poaEligibilityScore` (default 800).
Decentralization (WF-VM-03)
All thresholds live in `config.validator.*` and mirror the character definition at `agents/packages/agents/src/characters/validator-monitor.ts` so the deterministic pipeline and the narrative system prompt can never drift.
Design decisions
Deterministic numbers, narrative-only LLM calls. All scoring, Nakamoto, Gini, health, and whale detection happen in plain TypeScript. Claude is only invoked to write the report. Keeps the agent cheap, reproducible, and auditable.
Token-delta as delegation source for the MVP. WF-VM-02 snapshots each validator's `tokens` field every cycle and derives flows from the delta against the previous snapshot. A follow-up PR will plug in a real `MsgDelegate` / `MsgUndelegate` / `MsgRedelegate` tx-stream client. The code path swaps cleanly once real events are available.
Governance participation scoring is MVP-zero. The operator→delegator bech32 conversion (required to fetch per-validator votes via `/proposals/{id}/votes/{voter}`) is deferred to a follow-up PR. All validators currently receive a 0 governance score, so relative composite scores still surface real differences in uptime and stability without punishing anyone asymmetrically. A separate PR will add the bech32 conversion and full vote fetching.
Signing-info match falls back to assume-healthy. The MVP joins on the raw consensus key; when no match is found, the validator is assumed to have signed every block in the window. This under-counts real issues rather than smearing a healthy validator with a fake missed-blocks count.
Scorecards persisted every cycle. Every per-validator scorecard lands in the `scorecards` SQLite table so the historical timeline is usable for future PoA transition assessments.
Layer boundary
This agent operates at Layer 1 only — read-only, informational output, cannot delegate/undelegate/redelegate, cannot submit proposals or votes, cannot execute transactions. Matches the framework principle of starting with the lowest-risk, highest-value capability. Raising the automation layer (e.g., automated delegation rebalancing) is a separate governance decision.
Test plan
Scope not included (deliberate follow-ups)
Refs `phase-2/2.2-agentic-workflows.md` §WF-VM-01, §WF-VM-02, §WF-VM-03
Refs `agents/packages/agents/src/characters/validator-monitor.ts` (#64)