Skip to content

feat(core): reconcile extension cooldown (#510) and minimum-balance safety check (#504) - #656

Closed
EthTobi wants to merge 834 commits into
TegoLabs:mainfrom
EthTobi:feature/reconcile-cooldown-min-balance
Closed

EthTobi wants to merge 834 commits into
TegoLabs:mainfrom
EthTobi:feature/reconcile-cooldown-min-balance

Conversation

@EthTobi

@EthTobi EthTobi commented Aug 3, 2026

Copy link
Copy Markdown

Summary

This branch reconciles the two related extension-safety features so they can be merged in any order without conflicts. It is exactly upstream/main + extension cooldown (issue #510, PR #586) + minimum-balance safety check (issue #504, PR #587).

Extension cooldown (#510 / #586)

  • EXTENSION_COOLDOWN_MS (5 minutes) — prevents the same entry from being extended twice in quick succession by checking the most recent extension timestamp per entry (runs after the hourly rate-limit check; HOURLY_RATE_LIMIT is untouched).

Minimum-balance safety check (#504 / #587)

  • MINIMUM_BALANCE_XLM (5 XLM) — before submitting an extension through a channel account, verify it holds enough XLM for the base reserve + fees; skip with a warning and fire an alert through the contract's configured channels on unknown/low balance.
  • sorokeep channels list surfaces a LOW balance indicator for underfunded accounts.

How the checks compose in runAutoExtensions

  1. Hourly rate-limit check → 2. Per-entry cooldown filter (early exit if all entries are cooling down) → 3. Secret-key resolution → 4. Channel-account minimum-balance gate → 5. Extend the cooldown-eligible entries.

Validation

  • npm ci — clean install, 0 vulnerabilities (npm audit --audit-level=high and --omit=dev)
  • npm run lint — 0 errors
  • npm run test -- --coverage — 100 files / 1319 tests passing (includes the 2 new cooldown tests, 4 new min-balance tests, and 2 channels list low-balance tests)
  • npm run build — TypeScript compiles with zero errors
  • No merge-conflict markers; package-lock.json in sync with package.json

Merge guidance for maintainers

Resolves #510 and #504 together.

veloura-dev and others added 30 commits June 29, 2026 17:31
- Add docker-compose.devnet.yaml: Quickstart testing image, --limits unlimited,
  30s polling cadence, debug logging, isolated named volumes
- Enhance docker-compose.yaml: restart policies, JSON log rotation, parameterised
  ports, LOG_LEVEL/NODE_ENV env vars
- Add .env.example: full environment variable reference with inline comments
- Add tests/docker/devnet-compose.test.ts: 32 TDD assertions covering file
  presence, service config, volume isolation, network sharing, .env.example,
  and compose merge compatibility
- Update .dockerignore: exclude compose files, systemd/, docs/, templates/
- Update .gitignore: allow .env.example via negation rule

Acceptance criteria met: docker compose -f docker-compose.yaml
-f docker-compose.devnet.yaml up boots daemon and mock RPC environment
successfully.

All 530 tests pass, 63 docker-specific tests, 5 skipped TODOs.
…estimates

- Add countExtensionsInLastHour() to repositories.ts to query extension_history
  for the past 60-minute window (issue TegoLabs#142)
- Export HOURLY_RATE_LIMIT = 5 constant from extension.ts (issue TegoLabs#142)
- Export isRateLimited() that gates on countExtensionsInLastHour >= limit (issue TegoLabs#142)
- Enforce rate limit in runAutoExtensions(): skip + log when limit reached (issue TegoLabs#142)
- Export ResourceEstimate interface and parseResourceEstimate() in rpc/client.ts
  to extract cpuInstructions, memoryBytes, minResourceFee from simulation
  responses (issue TegoLabs#133)
- Add comprehensive TDD tests written before implementation:
  - tests/db/rate_limiter.test.ts: countExtensionsInLastHour edge cases
  - tests/core/rate_limiter.test.ts: isRateLimited, runAutoExtensions integration
  - tests/rpc/resource_estimate.test.ts: parseResourceEstimate + failure edge cases

Closes TegoLabs#133
Closes TegoLabs#137
Closes TegoLabs#142
AbdulmalikAlayande and others added 21 commits July 27, 2026 22:26
Central registration point for alert channel plugins. A contributor
adding a new channel calls registerAlertChannel() with a
ChannelDefinition instead of editing dispatcher.ts's channel map,
the CLI's --type if/else chain, and a DB CHECK constraint.
Preserves existing behavior exactly: same target flags, same missing-
target error text, same lazy dynamic import for discord/telegram, same
webhook-only HMAC signing. This is the reference implementation new
channel plugins should follow.
Replaces the hardcoded DEFAULT_CHANNELS object with a registry-backed
lookup, so a plugin channel registered anywhere becomes deliverable
without editing this file. Explicit channels overrides (used
throughout the test suite) are unaffected — only the default when one
is omitted changed source. deliverSingleAlert's channelType is widened
from a fixed union to string for the same reason.
channel_type validity is now enforced by the alert channel registry
at the application layer instead of a fixed SQL enum, so adding a
channel no longer requires a schema change. The CHECK now only
guards against an empty string.
…ration

The SCHEMA comment-stripper (`--.*\n`) silently failed to match
comments ending in \r\n, since JS's `.` excludes all line terminators
including \r. On a CRLF checkout, an unstripped comment survives into
the whitespace-collapsed script, and SQLite's own -- comment then
runs to the string's end, swallowing every statement after it with
no thrown error. Switched to `--[^\n]*\n`, which matches either line
ending. Latent since schema.sql had no comments before this change.

Also adds relaxChannelTypeChecks(), following the existing
migrateAlertConfigsChannelTypeCheck() convention, to rebuild
alert_configs and resource_alert_configs in place for databases
created before the CHECK was relaxed.
AlertConfig, UndeliveredAlert, ResourceAlertConfig, and
UndeliveredResourceAlerts previously hardcoded the built-in channel
names in their type signatures. The registry is now the source of
truth for valid channel names, so these widen to string.
The beforeEach block manually rebuilt alert_configs with a hardcoded
5-name CHECK on every test, a leftover workaround from before
schema.sql had these columns natively. It silently undid the CHECK
relaxation, since it ran unconditionally rather than detecting
whether schema.sql already had the change. getDatabaseForTesting()
already execs the current schema.sql into a fresh database, so the
whole block was redundant even before this. Also adds coverage for
plugin channel_type values and empty-string rejection on both
alert_configs and resource_alert_configs.
Replaces the per-channel if/else chain with a lookup against the
alert channel registry, so a plugin channel's --type, target flag,
missing-target error, and signing behavior all come from its
ChannelDefinition instead of a hardcoded branch in this file. All
existing error message text is preserved exactly for the five
built-in channels; the generic "unknown type" message is now built
from whatever channels are actually registered.
…nsion submission

- Add MINIMUM_BALANCE_XLM constant (5 XLM) — before submitting an extension
  through a channel account, verify it holds enough XLM to cover the base
  reserve and transaction fee.
- If balance is insufficient or unknown, skip submission, log a clear warning,
  and fire an alert through the contract's configured alert channels rather
  than attempting a failing transaction.
- Surface low-balance channel accounts in "sorokeep channels list" output
  with a visual balance indicator and LOW warning.

Resolves TegoLabs#504
…dundant back-to-back extensions

- Add EXTENSION_COOLDOWN_MS constant (5 minutes) — prevent the same entry
  from being extended twice in quick succession by checking the most
  recent extension timestamp per entry before auto-extending.
- Cooldown runs after the rate-limit check; both are complementary
  safeguards — HOURLY_RATE_LIMIT is not weakened or removed.
- An entry extended within the cooldown window is skipped with a clear
  log message; entries outside the window extend normally.

Resolves TegoLabs#510
# Conflicts:
#	.github/workflows/ci.yml
#	CONTRIBUTING.md
#	README.md
#	docs/ARCHITECTURE.md
#	docs/adding-an-alert-channel.md
#	package-lock.json
#	package.json
#	src/alerts/builtins.ts
#	src/alerts/dispatcher.ts
#	src/alerts/registry.ts
#	src/commands/alerts.ts
#	src/commands/daemon.ts
#	src/commands/db.ts
#	src/core/extension.ts
#	src/core/monitor.ts
#	src/daemon/loop.ts
#	src/db/backup.ts
#	src/db/database.ts
#	src/db/migrator.ts
#	src/db/repositories.ts
#	src/db/schema.sql
#	src/index.ts
#	src/lib.ts
#	src/rpc/client.ts
#	src/utils/config.ts
#	tests/alerts/builtins.test.ts
#	tests/alerts/dispatcher.test.ts
#	tests/alerts/slack.test.ts
#	tests/alerts/webhook.test.ts
#	tests/commands/alerts.test.ts
#	tests/commands/db.test.ts
#	tests/core/extension.test.ts
#	tests/core/monitor.test.ts
#	tests/core/rate_limiter.test.ts
#	tests/daemon/loop.test.ts
#	tests/db/backup.test.ts
#	tests/db/database.test.ts
#	tests/db/repositories.test.ts
#	tests/docker/docker-compose.test.ts
#	tests/e2e/sandbox-network.test.ts
#	tests/mcp/lifecycle.test.ts
#	tests/rpc/resource_estimate.test.ts
# Conflicts:
#	.github/workflows/ci.yml
#	CONTRIBUTING.md
#	README.md
#	docs/ARCHITECTURE.md
#	docs/adding-an-alert-channel.md
#	package-lock.json
#	package.json
#	src/alerts/builtins.ts
#	src/alerts/dispatcher.ts
#	src/alerts/registry.ts
#	src/commands/alerts.ts
#	src/commands/channels.ts
#	src/commands/daemon.ts
#	src/commands/db.ts
#	src/core/extension.ts
#	src/core/monitor.ts
#	src/daemon/loop.ts
#	src/db/backup.ts
#	src/db/database.ts
#	src/db/migrator.ts
#	src/db/repositories.ts
#	src/db/schema.sql
#	src/index.ts
#	src/lib.ts
#	src/rpc/client.ts
#	src/utils/config.ts
#	tests/alerts/builtins.test.ts
#	tests/alerts/dispatcher.test.ts
#	tests/alerts/slack.test.ts
#	tests/alerts/webhook.test.ts
#	tests/commands/alerts.test.ts
#	tests/commands/channels.test.ts
#	tests/commands/db.test.ts
#	tests/core/extension.test.ts
#	tests/core/monitor.test.ts
#	tests/daemon/loop.test.ts
#	tests/db/backup.test.ts
#	tests/db/database.test.ts
#	tests/db/repositories.test.ts
#	tests/docker/docker-compose.test.ts
#	tests/e2e/sandbox-network.test.ts
#	tests/mcp/lifecycle.test.ts
#	tests/rpc/resource_estimate.test.ts
# Conflicts:
#	src/core/extension.ts
#	tests/core/extension.test.ts
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Channel listings now show each account’s XLM balance.
    • Low balances are marked LOW; unavailable balances appear as unknown.
    • Automatic channel extensions now require sufficient balance and avoid recently extended entries.
    • Low-balance channels release their slot and trigger configured alerts.
  • Bug Fixes

    • Prevented extensions for channels with unknown or insufficient XLM balances.
    • Improved extension timing and cooldown handling.

Walkthrough

The channel listing command now shows XLM balance status. Auto-extension filters recently extended entries and validates channel-account balances before submission. Low-balance cases record errors, release channel slots, and dispatch configured alerts.

Changes

Channel Extension Eligibility

Layer / File(s) Summary
Balance thresholds and channel reporting
src/core/extension.ts, src/commands/channels.ts, tests/commands/channels.test.ts
Defines the minimum XLM balance and displays LOW, normal, or unknown balance states in channels list.
Per-entry cooldown filtering
src/core/extension.ts, tests/core/extension.test.ts, tests/core/rate_limiter.test.ts
Excludes entries extended within the cooldown period and tests eligible and ineligible extension history.
Balance-gated extension execution
src/core/extension.ts, tests/core/extension.test.ts
Skips unknown or insufficient balances, records errors, releases channel slots, dispatches alerts, and allows sufficiently funded extensions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant runAutoExtensions
  participant ExtensionHistory
  participant ChannelAccount
  participant AlertDispatcher
  participant TransactionSubmission
  runAutoExtensions->>ExtensionHistory: read recent extension history
  ExtensionHistory-->>runAutoExtensions: return cooldown-eligible entries
  runAutoExtensions->>ChannelAccount: check XLM balance
  ChannelAccount-->>runAutoExtensions: return balance status
  runAutoExtensions->>AlertDispatcher: dispatch low-balance alert
  runAutoExtensions->>TransactionSubmission: submit eligible extension
Loading

Possibly related issues

  • Issue 504: Covers minimum XLM balance checks, low-balance channel display, skipped extensions, and alert dispatch.

Possibly related PRs

Suggested reviewers: abdulmalikalayande

Poem

A rabbit checks the channel gate,
“Five XLM must guard the slate.”
Cooldown keeps quick repeats away,
Low funds call an alert today.
With balance sound, extensions hop—
Safe transactions never stop.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Minimum-balance and channel-list changes are outside the provided [#510] scope, which limits edits to the cooldown logic and tests. Move minimum-balance and channel-list changes to a PR linked to the relevant issue, or link that issue and update the approved scope.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the per-entry extension cooldown and minimum XLM balance safety changes.
Description check ✅ Passed The description directly explains the cooldown, balance check, alert, channel-list, and validation changes.
Linked Issues check ✅ Passed The implementation adds cooldown filtering, preserves hourly rate limiting, and tests entries inside and outside the cooldown window for [#510].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.6)
src/core/extension.ts

File contains syntax errors that prevent linting: Line 17: Declarations inside of a import declaration may not have duplicates


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/extension.ts`:
- Around line 389-405: The executedAt parsing in the recentHistory loop must
preserve timestamps that already include a trailing Z or numeric timezone
offset. Update the conversion before lastExtendedAt.set to append Z only for
timezone-less SQLite timestamps, and add a cooldown test using an ISO UTC
timestamp to verify eligibility is calculated correctly.

In `@tests/core/extension.test.ts`:
- Around line 941-983: Update the “extends entry normally when outside the
cooldown window” test to insert a prior extension-history record for the seeded
entry, using an executed_at timestamp older than EXTENSION_COOLDOWN_MS. Keep the
existing entry, policy, mocks, and assertions unchanged so the test specifically
exercises an entry whose cooldown has expired.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 85850b4f-d8fd-4093-a110-65a8a0a83495

📥 Commits

Reviewing files that changed from the base of the PR and between 9da92c6 and 4ea0ef3.

📒 Files selected for processing (5)
  • src/commands/channels.ts
  • src/core/extension.ts
  • tests/commands/channels.test.ts
  • tests/core/extension.test.ts
  • tests/core/rate_limiter.test.ts
📜 Review details
🧰 Additional context used
🪛 OpenGrep (1.26.0)
tests/core/extension.test.ts

[ERROR] 783-786: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (6)
src/core/extension.ts (2)

9-9: LGTM!

Also applies to: 39-57, 446-504


384-421: 🗄️ Data Integrity & Integration

No change is required for getExtensionHistory. The days parameter treats 1 as a one-day time window, not a result limit. The query returns all matching records, including recent extensions for every entry.

			> Likely an incorrect or invalid review comment.
src/commands/channels.ts (1)

6-6: LGTM!

Also applies to: 60-73

tests/commands/channels.test.ts (1)

6-6: LGTM!

Also applies to: 227-267

tests/core/extension.test.ts (1)

11-13: LGTM!

Also applies to: 39-45, 782-787, 897-939, 985-1161

tests/core/rate_limiter.test.ts (1)

94-101: LGTM!

Comment thread src/core/extension.ts
Comment on lines +389 to +405
for (const record of recentHistory) {
const entryId = record.contract_entry_id;
// executed_at is stored as UTC (SQLite CURRENT_TIMESTAMP); append
// "Z" so the timestamp is parsed as UTC regardless of the host
// machine's timezone.
const executedAt = new Date(record.executed_at + "Z");
const existing = lastExtendedAt.get(entryId);
if (!existing || executedAt > existing) {
lastExtendedAt.set(entryId, executedAt);
}
}

const cooldownEligible = needsExtension.filter(e => {
const lastExt = lastExtendedAt.get(e.id);
if (!lastExt) return true; // never extended → eligible
const elapsed = Date.now() - lastExt.getTime();
if (elapsed < EXTENSION_COOLDOWN_MS) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- source locations ---'
rg -n -C 12 'recentHistory|executed_at|EXTENSION_COOLDOWN_MS|cooldownEligible|lastExtendedAt' src/core/extension.ts

printf '%s\n' '--- test timestamp data ---'
rg -n -C 12 'executed_at|CURRENT_TIMESTAMP|rate.?limiter|cooldown|extended' tests/core/rate_limiter.test.ts

printf '%s\n' '--- related timestamp writes and history query ---'
rg -n -C 8 'contract_entry_id|executed_at|extension_history|recentHistory' src tests

Repository: AbdulmalikAlayande/sorokeep

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -eu

node <<'JS'
const inputs = [
  "2026-08-15 12:00:00",
  "2026-08-15 12:00:00Z",
  "2026-08-15T12:00:00.000Z",
  "2026-08-15T12:00:00.000ZZ",
  "2026-08-15T12:00:00+00:00",
  "2026-08-15T12:00:00.000+05:30",
];

for (const input of inputs) {
  const parsed = new Date(input + "Z");
  console.log(JSON.stringify({
    input,
    appended: input + "Z",
    time: parsed.getTime(),
    isValid: !Number.isNaN(parsed.getTime()),
    iso: Number.isNaN(parsed.getTime()) ? null : parsed.toISOString(),
  }));
}

const cooldownMs = 5 * 60 * 1000;
const now = Date.parse("2026-08-15T12:04:00.000Z");
for (const lastExt of [
  new Date("2026-08-15T12:00:00.000Z"),
  new Date("2026-08-15T12:00:00.000ZZ"),
]) {
  const elapsed = now - lastExt.getTime();
  console.log(JSON.stringify({
    lastExtTime: lastExt.getTime(),
    elapsed,
    blocked: elapsed < cooldownMs,
    eligible: !(elapsed < cooldownMs),
  }));
}
JS

printf '%s\n' '--- targeted test sections ---'
sed -n '80,110p' tests/core/rate_limiter.test.ts
sed -n '235,340p' tests/core/rate_limiter.test.ts

printf '%s\n' '--- schema and repository definitions ---'
sed -n '90,110p' src/db/schema.sql
sed -n '469,505p' src/db/repositories.ts

Repository: AbdulmalikAlayande/sorokeep

Length of output: 7197


Normalize timezone-qualified executed_at values

When executed_at ends with Z or a numeric offset, parse it without appending another Z. Append Z only to timezone-less SQLite timestamps. Add a cooldown test with an ISO UTC timestamp.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/extension.ts` around lines 389 - 405, The executedAt parsing in the
recentHistory loop must preserve timestamps that already include a trailing Z or
numeric timezone offset. Update the conversion before lastExtendedAt.set to
append Z only for timezone-less SQLite timestamps, and add a cooldown test using
an ISO UTC timestamp to verify eligibility is calculated correctly.

Comment on lines +941 to +983
it("extends entry normally when outside the cooldown window", async () => {
const contractId = seedContract(db);

upsertEntry(db, {
contract_id: contractId,
entry_key_xdr: "instance-key-xdr",
entry_type: "instance",
live_until_ledger: 2410000,
discovery_source: "deterministic",
});

upsertExtensionPolicy(db, {
contract_id: contractId,
enabled: true,
target_ttl_ledgers: 100000,
extend_when_below_ledgers: 20000,
keypair_source: "env:TEST_SECRET_KEY",
});

setEnv("TEST_SECRET_KEY", "SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");

mockGetCurrentLedger.mockResolvedValue(2400000);
mockSubmitExtension.mockResolvedValue({
success: true,
txHash: "cooldown-outside-tx",
ledger: 2400100,
});
mockGetEntryTTLs.mockResolvedValue({
latestLedger: 2400100,
entries: [{
entryKeyXdr: "instance-key-xdr",
latestLedger: 2400100,
liveUntilLedgerSeq: 2500100,
lastModifiedLedgerSeq: 2400100,
remainingTTL: 100000,
}],
});

const result = await runAutoExtensions(db, "testnet");

expect(result.contractsExtended).toBe(1);
expect(result.extensions[0]!.txHash).toBe("cooldown-outside-tx");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Seed expired history in the cooldown-expiry test.

This test creates no prior extension record. It tests a never-extended entry, not an entry outside the cooldown window. Insert history for the entry and backdate executed_at beyond EXTENSION_COOLDOWN_MS.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/extension.test.ts` around lines 941 - 983, Update the “extends
entry normally when outside the cooldown window” test to insert a prior
extension-history record for the seeded entry, using an executed_at timestamp
older than EXTENSION_COOLDOWN_MS. Keep the existing entry, policy, mocks, and
assertions unchanged so the test specifically exercises an entry whose cooldown
has expired.

AbdulmalikAlayande added a commit that referenced this pull request Sep 5, 2026
…504, PR #587/#656)

Adds MINIMUM_BALANCE_XLM (5 XLM) — before submitting an extension
through a channel account, verify it holds enough XLM to cover the
base reserve and transaction fee. If the balance is insufficient or
unknown, skip submission, log a warning, and fire an alert through
the contract's configured channels rather than attempting a
transaction that would likely fail or drain the account below reserve.

Surfaces low-balance channel accounts in 'sorokeep channels list'
with a balance indicator and LOW warning.

Ported from PR #656 (a reconciled combination of #587/min-balance and
#586/cooldown, both by the same contributor) rather than either
individual PR, since #656 was explicitly built to merge conflict-free
regardless of order and represented the contributor's own final,
tested state — including two real fixes they'd already made (UTC
timestamp parsing in the cooldown check, and clamping remainingTTL to
zero in the low-balance alert payload). Applied only the min-balance
delta here; the cooldown feature (#510) is a separate issue handled
on its own.
@AbdulmalikAlayande

Copy link
Copy Markdown
Collaborator

Thanks for the reconciliation work — made merging both features independently straightforward. Applied the min-balance delta from this branch as PR #587's implementation (see comment there). Closing this coordination PR since both underlying issues (#504, #510) are tracked individually.

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.

feat(core): add a configurable extension cooldown per entry to avoid redundant back-to-back extensions