Skip to content

fix(cloud): reject prefix-parsed telemetry thresholds - #24131

Merged
lalalune merged 2 commits into
elizaOS:developfrom
Svector-anu:fix/cloud-telemetry-threshold-strict
Aug 22, 2026
Merged

lalalune merged 2 commits into
elizaOS:developfrom
Svector-anu:fix/cloud-telemetry-threshold-strict

Conversation

@Svector-anu

@Svector-anu Svector-anu commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Relates to

No separate issue — the defect is described in full below.

Definition of Done: full standard in CONTRIBUTING.md.

  • This PR targets develop and is built directly on the current origin/develop tree, so it applies with zero conflicts.
  • Focused package tests, typecheck and lint were run for the changed files; results are quoted below. Root bun run verify was not run green: develop currently carries unrelated @elizaos/agent Biome formatter diagnostics, the same blocker recorded in fix(voice): restore immediate prewarmed phone turns #24174.
  • A reviewer can confirm the change from the failure/mutation output below without reading the code.

Sync with develop

  • Built on the latest origin/develop tree at review time; zero conflicts.
  • Exact unrelated blocker documented above (pre-existing @elizaos/agent formatter diagnostics).

Risks

Low. Observability thresholds only. Valid thresholds are unchanged; malformed ones now take the documented fallback instead of being published as configured.

Background

numberEnv accepts a value parseInt merely starts with:

const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;

parseInt stops at the first non-digit, so CLOUD_SLOW_DB_MS=500junk yields 500 — finite and positive, so the documented fallback never runs. NaN is the only malformed input this guard actually catches.

All three observability thresholds go through it — CLOUD_SLOW_REQUEST_MS, CLOUD_SLOW_DB_MS, CLOUD_DB_BURST_COUNT. They decide which calls are classified slow or bursty:

slowDb: db.filter((r) => r.durationMs >= t.slowDbMs),

…and getCloudTelemetrySnapshot also publishes the thresholds it used:

return { generatedAt: nowIso(), thresholds: t, ... };

So a typo does not just misclassify — it makes the telemetry report state a boundary nobody configured, while presenting it as the configured one. Observability that misreports its own parameters is worse than observability that is absent, because the numbers still look authoritative.

The fix

Require the whole trimmed value to be decimal before converting, so malformed input reaches the fallback the function already defines. Number.isSafeInteger replaces Number.isFinite so a value past 2^53 is rejected too. Valid thresholds are unaffected.

Verification

Gate Result
cloud-backend-observability.test.ts 5 passed
biome check (both changed files) clean

Drift-checked against current develop: numberEnv is unchanged upstream, so the defect is live.

Mutation resistance

Reverting only the source change and keeping the tests:

error: expect(received).toBe(expected)
Expected: 250
Received: 500
(fail) ignores a trailing-garbage threshold instead of publishing its prefix

The failure is the snapshot publishing the prefix-parsed threshold. The "still honours a clean threshold" case passes in both directions, so the fix is specific to malformed input.

Attribution

  • Found by OpenAI gpt-5.6-sol via Codex (AgentRouter) during a numeric-input validation sweep of packages/cloud.
  • Independent verification, the snapshot-publishes-its-own-thresholds framing, the safe-integer bound, and the mutation proof by Claude Code (claude-opus-5).

Documentation changes needed?

My changes do not require a change to the project documentation — the parser's documented contract is unchanged; this makes the implementation match it.

Testing

Where should a reviewer start?

Start with the mutation-resistance block below: it reverts only the source change and shows the exact wrong value the current code produces.

Detailed testing steps

None beyond the automated suites quoted above — the change is a pure input-parsing boundary and is fully covered by the regression tests.

Evidence Gate

  • Before screenshots N/A - backend-only change to the cloud telemetry threshold parsers, no UI surface.
  • After screenshots N/A - backend-only change to the cloud telemetry threshold parsers, no UI surface.
  • A video walkthrough N/A - the observable behaviour is the thresholds published in the telemetry snapshot, shown by the mutation-proof output quoted below.
  • Backend logs N/A - no service is started by this change; the exercised path is covered by the unit suite quoted below.
  • Frontend console/network logs N/A - no frontend change.
  • Real-LLM trajectory N/A - no model, prompt, provider or action path is changed; this is an input parser.
  • Domain artifacts N/A - no domain record is produced; the observable output is the thresholds published in the telemetry snapshot, asserted in the tests.
  • OCR visual-text review N/A - the change has no rendered visual surface.

@lalalune lalalune left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 0cefd1b8841ec04c0af9768cf91c0b027960dac3.

The strict whole-string validation is directionally correct, but /^\d+$/ introduces a compatibility regression: the existing Number.parseInt(raw, 10) accepts an explicitly positive value such as +500, while the replacement silently falls back. Please preserve the optional leading plus (for example, validate a signed whole integer and retain the existing parsed > 0 constraint, or use an optional-plus pattern).

Please also add regression coverage for:

  • +500 remaining a valid positive threshold
  • an integer above Number.MAX_SAFE_INTEGER falling back

The latter is required because this patch changes the acceptance predicate from finite to safe integer and the PR explicitly claims that boundary behavior, but the new tests currently exercise only trailing garbage and an ordinary positive integer.

Hosted checks are queued. This source blocker is the reason for the changes-requested disposition; the PR should remain ready for review.

@Svector-anu
Svector-anu force-pushed the fix/cloud-telemetry-threshold-strict branch from 0cefd1b to f4e2146 Compare August 21, 2026 23:15
@Svector-anu

Copy link
Copy Markdown
Contributor Author

Fixed at f4e21465. You were right — /^\d+$/ rejected +500, which Number.parseInt accepted, so the patch traded a prefix-parse bug for a compatibility regression. Now /^\+?\d+$/, with the parsed > 0 constraint unchanged.

Both requested regressions are in: +500 resolves to 500, and 9007199254740993 falls back to the 250ms default — the safe-integer boundary this patch claims. 7 passed.

This applied to five sibling PRs, so I corrected all of them rather than just this one — same /^\+?\d+$/ change and the same two test cases:

One further note: #23970 already merged with this same regression in the four electrobun helpers. I will open a separate follow-up for that rather than leave it in develop.

While reworking #24143 I also caught that a bulk edit had touched a pre-existing /^\d+$/ in compareDiscordSnowflake — unrelated to the fix. That is reverted; the branch now differs from develop only in the two intended helpers.

@standujar

Copy link
Copy Markdown
Collaborator

CLAIMING REVIEW: exact-head adversarial re-review of the cloud telemetry threshold parser and requested regression coverage on f4e21465ebe2559a54c446cca81339344df9ec33.

@standujar standujar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head f4e21465ebe2559a54c446cca81339344df9ec33.

The earlier semantic requests are fixed: +500 remains accepted, the first integer above Number.MAX_SAFE_INTEGER falls back, trimming and all three threshold keys behave consistently, and environment reads remain live rather than cached. The PR's 7 tests plus 31 independent adversarial grammar/bounds/isolation cases pass (38/38) under Bun 1.3.14 in a credential-free, network-denied OS sandbox. git diff --check passes, and merge synthesis with current develop@06480e1ca7e86e57cd86627188738aa6b142c9f8 is conflict-free (tree 8783c908dbed6c7f67906588bac4e4b280272483).

Two P3 formatter findings remain on newly added lines. With repository-pinned Biome 2.5.8, the exact command biome check packages/cloud/shared/src/lib/observability/cloud-backend-observability.ts packages/cloud/shared/src/lib/observability/cloud-backend-observability.test.ts reports four diffs: two are already present unchanged on current develop (observeCloudRequest and recordCloudStreamMilestones), while the two inline findings below exist only on this PR head. Please format the two introduced lines; this review does not attribute the two baseline diffs to the PR. No hosted checks are currently reported for this head.

AI provider/model: OpenAI / gpt-5.6-sol
Client / agent tooling: Codex desktop
Contribution skill revision: f4e2146:packages/skills/skills/contribute-to-eliza
Attribution status: self-reported
— [standujar/review-24131]

// The optional leading plus is kept deliberately: `Number.parseInt` accepted
// "+500", so rejecting it here would be a compatibility regression rather
// than a fix.
const parsed = trimmed && /^\+?\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Format this newly added conditional with the repository-pinned Biome 2.5.8. The exact targeted check requires the assignment to wrap after =; this diff is additional to the two unrelated formatter failures already present on current develop.

});

describe("telemetry threshold env parsing", () => {
const KEYS = ["CLOUD_SLOW_DB_MS", "CLOUD_SLOW_REQUEST_MS", "CLOUD_DB_BURST_COUNT"];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Format this newly added key list with the repository-pinned Biome 2.5.8. The targeted check requires the three entries on separate lines; current develop does not contain this formatter failure.

@Svector-anu

Copy link
Copy Markdown
Contributor Author

Evidence rows added and validated locally with scripts/check-pr-evidence.mjs at exact head, including --changed-files-file from the real PR diff:

Evidence gate passed: all required rows satisfied.

Applied to every PR of mine that was missing them — #23979, #23995, #24001, #24008, #24124, #24131, #24143, #24188, #24193 — each with its own evidence-head marker and rows written for that specific change rather than copied.

Two I deliberately did not touch: #23950 and #23951 still show Pending on llm-trajectory and domain-artifacts, because you set those rows explicitly and called out the live-model action trajectory as required for the changed action path. That is a real capture I cannot fake with an N/A, so I left your rows as written.

For the rest the N/A reasons are narrow and checkable — e.g. on #23979 the two changed files are a non-rendering .ts hook and its .test.tsx, neither of which the checker counts as a rendered-UI surface (SURFACE_VISUAL_EXT_RE excludes .ts, and *.test.tsx is explicitly excluded), which is why I validated with --changed-files-file rather than asserting it.

@lalalune
lalalune marked this pull request as draft August 22, 2026 03:54
lalalune pushed a commit to Svector-anu/eliza that referenced this pull request Aug 22, 2026
Follow-up to elizaOS#23970, which is already merged. That change tightened four
integer env parsers to reject trailing garbage, but used `/^\d+$/` — which also
rejects an explicitly signed value:

    ELIZA_RENDERER_PROXY_IDLE_TIMEOUT_SECONDS=+30

`Number.parseInt("+30", 10)` returned `30`, so that configuration worked before
elizaOS#23970 and silently falls back to the default now. That is a compatibility
regression introduced by the fix, not by the original defect.

Fix: allow the optional leading plus in all four parsers (`/^\+?\d+$/`). The
trailing-garbage rejection elizaOS#23970 added is unchanged, and so is the
`Number.isSafeInteger` bound.

Also adds the coverage that would have caught this: each affected parser now
asserts a signed positive value still resolves, and that an integer beyond
`Number.MAX_SAFE_INTEGER` falls back — the boundary elizaOS#23970 introduced but did
not test.

Caught in review of the sibling PRs applying the same pattern (thanks
@lalalune on elizaOS#24131). The five unmerged siblings — elizaOS#23995, elizaOS#24001, elizaOS#24008,
elizaOS#24120, elizaOS#24143 — were corrected on their own branches; this PR repairs the one
that had already landed.

Verification
- 4 affected suites: 21 passed
- full electrobun lane: 586 passed across 76 files
- `biome check` on all six changed files: clean

Mutation resistance
- Restoring develop's current sources and keeping the new tests fails with
  `expected 255 to be 30` (renderer proxy idle timeout) and
  `expected 240 to be 120` (TTS chunk limit) — the signed values falling back
  instead of being honoured.

Attribution
- Regression identified by @lalalune during review of elizaOS#24131.
- Repair, cross-branch audit, and mutation proof by Claude Code
  (`claude-opus-5`).

Claude-Session: https://claude.ai/code/session_011wVLgS8jLiPytQVLnqwLZL
lalalune pushed a commit to Svector-anu/eliza that referenced this pull request Aug 22, 2026
Follow-up to elizaOS#23970, which is already merged. That change tightened four
integer env parsers to reject trailing garbage, but used `/^\d+$/` — which also
rejects an explicitly signed value:

    ELIZA_RENDERER_PROXY_IDLE_TIMEOUT_SECONDS=+30

`Number.parseInt("+30", 10)` returned `30`, so that configuration worked before
elizaOS#23970 and silently falls back to the default now. That is a compatibility
regression introduced by the fix, not by the original defect.

Fix: allow the optional leading plus in all four parsers (`/^\+?\d+$/`). The
trailing-garbage rejection elizaOS#23970 added is unchanged, and so is the
`Number.isSafeInteger` bound.

Also adds the coverage that would have caught this: each affected parser now
asserts a signed positive value still resolves, and that an integer beyond
`Number.MAX_SAFE_INTEGER` falls back — the boundary elizaOS#23970 introduced but did
not test.

Caught in review of the sibling PRs applying the same pattern (thanks
@lalalune on elizaOS#24131). The five unmerged siblings — elizaOS#23995, elizaOS#24001, elizaOS#24008,
elizaOS#24120, elizaOS#24143 — were corrected on their own branches; this PR repairs the one
that had already landed.

Verification
- 4 affected suites: 21 passed
- full electrobun lane: 586 passed across 76 files
- `biome check` on all six changed files: clean

Mutation resistance
- Restoring develop's current sources and keeping the new tests fails with
  `expected 255 to be 30` (renderer proxy idle timeout) and
  `expected 240 to be 120` (TTS chunk limit) — the signed values falling back
  instead of being honoured.

Attribution
- Regression identified by @lalalune during review of elizaOS#24131.
- Repair, cross-branch audit, and mutation proof by Claude Code
  (`claude-opus-5`).

Claude-Session: https://claude.ai/code/session_011wVLgS8jLiPytQVLnqwLZL
lalalune pushed a commit to Svector-anu/eliza that referenced this pull request Aug 22, 2026
Follow-up to elizaOS#23970, which is already merged. That change tightened four
integer env parsers to reject trailing garbage, but used `/^\d+$/` — which also
rejects an explicitly signed value:

    ELIZA_RENDERER_PROXY_IDLE_TIMEOUT_SECONDS=+30

`Number.parseInt("+30", 10)` returned `30`, so that configuration worked before
elizaOS#23970 and silently falls back to the default now. That is a compatibility
regression introduced by the fix, not by the original defect.

Fix: allow the optional leading plus in all four parsers (`/^\+?\d+$/`). The
trailing-garbage rejection elizaOS#23970 added is unchanged, and so is the
`Number.isSafeInteger` bound.

Also adds the coverage that would have caught this: each affected parser now
asserts a signed positive value still resolves, and that an integer beyond
`Number.MAX_SAFE_INTEGER` falls back — the boundary elizaOS#23970 introduced but did
not test.

Caught in review of the sibling PRs applying the same pattern (thanks
@lalalune on elizaOS#24131). The five unmerged siblings — elizaOS#23995, elizaOS#24001, elizaOS#24008,
elizaOS#24120, elizaOS#24143 — were corrected on their own branches; this PR repairs the one
that had already landed.

Verification
- 4 affected suites: 21 passed
- full electrobun lane: 586 passed across 76 files
- `biome check` on all six changed files: clean

Mutation resistance
- Restoring develop's current sources and keeping the new tests fails with
  `expected 255 to be 30` (renderer proxy idle timeout) and
  `expected 240 to be 120` (TTS chunk limit) — the signed values falling back
  instead of being honoured.

Attribution
- Regression identified by @lalalune during review of elizaOS#24131.
- Repair, cross-branch audit, and mutation proof by Claude Code
  (`claude-opus-5`).

Claude-Session: https://claude.ai/code/session_011wVLgS8jLiPytQVLnqwLZL
lalalune pushed a commit that referenced this pull request Aug 22, 2026
…nvs (#24188)

* fix(app-core): keep accepting a signed integer in electrobun env parsing

Follow-up to #23970, which is already merged. That change tightened four
integer env parsers to reject trailing garbage, but used `/^\d+$/` — which also
rejects an explicitly signed value:

    ELIZA_RENDERER_PROXY_IDLE_TIMEOUT_SECONDS=+30

`Number.parseInt("+30", 10)` returned `30`, so that configuration worked before
#23970 and silently falls back to the default now. That is a compatibility
regression introduced by the fix, not by the original defect.

Fix: allow the optional leading plus in all four parsers (`/^\+?\d+$/`). The
trailing-garbage rejection #23970 added is unchanged, and so is the
`Number.isSafeInteger` bound.

Also adds the coverage that would have caught this: each affected parser now
asserts a signed positive value still resolves, and that an integer beyond
`Number.MAX_SAFE_INTEGER` falls back — the boundary #23970 introduced but did
not test.

Caught in review of the sibling PRs applying the same pattern (thanks
@lalalune on #24131). The five unmerged siblings — #23995, #24001, #24008,
#24120, #24143 — were corrected on their own branches; this PR repairs the one
that had already landed.

Verification
- 4 affected suites: 21 passed
- full electrobun lane: 586 passed across 76 files
- `biome check` on all six changed files: clean

Mutation resistance
- Restoring develop's current sources and keeping the new tests fails with
  `expected 255 to be 30` (renderer proxy idle timeout) and
  `expected 240 to be 120` (TTS chunk limit) — the signed values falling back
  instead of being honoured.

Attribution
- Regression identified by @lalalune during review of #24131.
- Repair, cross-branch audit, and mutation proof by Claude Code
  (`claude-opus-5`).

Claude-Session: https://claude.ai/code/session_011wVLgS8jLiPytQVLnqwLZL

* test(app-core): cover every Electrobun integer parser

---------

Co-authored-by: moon <autonomousresearcher@gmail.com>
@Svector-anu
Svector-anu force-pushed the fix/cloud-telemetry-threshold-strict branch from f4e2146 to e7e5047 Compare August 22, 2026 06:34
@Svector-anu
Svector-anu force-pushed the fix/cloud-telemetry-threshold-strict branch from e7e5047 to e3481ba Compare August 22, 2026 18:04
`numberEnv` accepts a value `parseInt` merely starts with:

    const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
    return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;

`parseInt` stops at the first non-digit, so `CLOUD_SLOW_DB_MS=500junk` yields
`500` — finite and positive, so the documented fallback never runs. `NaN` is the
only malformed input this guard actually catches.

All three observability thresholds go through it: `CLOUD_SLOW_REQUEST_MS`,
`CLOUD_SLOW_DB_MS` and `CLOUD_DB_BURST_COUNT`. They decide which calls are
classified slow or bursty in `getCloudTelemetrySnapshot`:

    slowDb: db.filter((r) => r.durationMs >= t.slowDbMs),

and the snapshot also *publishes* the thresholds it used. So a typo does not
just misclassify — it makes the telemetry report state a boundary nobody
configured, while presenting it as the configured one. Observability that
misreports its own parameters is worse than observability that is absent,
because the numbers still look authoritative.

Fix: require the whole trimmed value to be decimal before converting, so
malformed input reaches the fallback the function already defines.
`Number.isSafeInteger` replaces `Number.isFinite` so a value past 2^53 is
rejected too. Valid thresholds are unaffected.

Verification
- `cloud-backend-observability.test.ts`: 5 passed
- `biome check` on both changed files: clean
- Drift-checked against current `develop`: `numberEnv` is unchanged upstream, so
  the defect is live.

Mutation resistance
- Reverting only the source change and keeping the tests fails with
  `Expected: 250, Received: 500` — the snapshot publishing the prefix-parsed
  threshold.
- The "still honours a clean threshold" case passes in both directions, so the
  fix is specific to malformed input.

Attribution
- Found by OpenAI `gpt-5.6-sol` via Codex (AgentRouter) during a numeric-input
  validation sweep of packages/cloud.
- Independent verification, the snapshot-publishes-its-own-thresholds framing,
  the safe-integer bound, and the mutation proof by Claude Code
  (`claude-opus-5`).

Claude-Session: https://claude.ai/code/session_011wVLgS8jLiPytQVLnqwLZL

Review follow-up: preserve the optional leading plus. Number.parseInt accepted
"+500", so requiring ^\d+$ would have rejected a value the old code took — a
compatibility regression rather than a fix. Added regression coverage for a
signed positive value and for an integer beyond Number.MAX_SAFE_INTEGER, which
the safe-integer predicate this patch introduces now rejects.

Review follow-up: preserve the optional leading plus. Number.parseInt accepted
"+500", so requiring ^\d+$ would have rejected a value the old code took — a
compatibility regression rather than a fix. Added regression coverage for a
signed positive value and for an integer beyond Number.MAX_SAFE_INTEGER, which
the safe-integer predicate this patch introduces now rejects.
@Svector-anu
Svector-anu force-pushed the fix/cloud-telemetry-threshold-strict branch from e3481ba to b02fa68 Compare August 22, 2026 18:55
@lalalune
lalalune marked this pull request as ready for review August 22, 2026 19:51
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Svector-anu

Copy link
Copy Markdown
Contributor Author

@standujar Thanks for the adversarial run — the 38/38 confirmation on the semantic contract is appreciated. On the two P3 formatter findings, I can't reproduce them with repository-pinned Biome 2.5.8, and I believe applying them would break the check rather than fix it.

packages/cloud/shared/biome.json is a nested config ("root": false, line 2) and sets formatter.lineWidth: 100. The repository root biome.json sets no lineWidth, so it uses Biome's default of 80. Biome 2.x resolves the nearest non-root config for files underneath it, so both files are formatted at 100, not 80:

cloud-backend-observability.ts:117    84 chars
cloud-backend-observability.test.ts:77  85 chars

Both fall between the two limits, which is exactly the band where the root default and the package config disagree.

At the head you reviewed (f4e21465) and at the current head (2e21e8ae) — the two files are byte-identical between them — Biome 2.5.8 reports clean:

$ bunx @biomejs/biome@2.5.8 check packages/cloud/shared/src/lib/observability/cloud-backend-observability.ts \
                                   packages/cloud/shared/src/lib/observability/cloud-backend-observability.test.ts
Checked 2 files in 14ms. No fixes applied.
EXIT=0

Also clean with --config-path=. forced to the root config, and clean under biome format.

I did apply both requested edits to be sure, then ran the command CI actually runs for this package (bunx @biomejs/biome check . from packages/cloud/shared, per its lint:check script). Both become errors:

× Formatter would have printed the following content:
   77 │ - ··const·KEYS·=·[
   78 │ - ····"CLOUD_SLOW_DB_MS",
   ...
      77 │ + ··const·KEYS·=·["CLOUD_SLOW_DB_MS",·"CLOUD_SLOW_REQUEST_MS",·"CLOUD_DB_BURST_COUNT"];

× Formatter would have printed the following content:
  117 │ - ··const·parsed·=
  118 │ - ····trimmed·&&·/^\+?\d+$/.test(trimmed)·?·Number(trimmed)·:·Number.NaN;
      117 │ + ··const·parsed·=·trimmed·&&·/^\+?\d+$/.test(trimmed)·?·Number(trimmed)·:·Number.NaN;

Found 2 errors.

Biome wants both lines joined — which is how they already are on this head. So I've left them as-is. If your run resolved the root config for these paths (a --config-path pointing at the repo root, or a checkout missing the nested file), that would produce exactly the two diffs you saw; happy to change it if you can reproduce with the package-scoped command.

Hosted CI on the current head is green ("All Tests Passed"). @lalalune — your two source blockers were fixed at head f4e21465: +500 stays accepted via /^\+?\d+$/, and there is regression coverage for a signed positive value and for the first integer above Number.MAX_SAFE_INTEGER falling back.

@lalalune
lalalune merged commit 6520b5d into elizaOS:develop Aug 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants