Repository navigation
Conversation
- 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.
…des and acceptance criteria
…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
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
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe 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. ChangesChannel Extension Eligibility
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
Possibly related issues
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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.tsFile contains syntax errors that prevent linting: Line 17: Declarations inside of a 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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
src/commands/channels.tssrc/core/extension.tstests/commands/channels.test.tstests/core/extension.test.tstests/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 & IntegrationNo change is required for
getExtensionHistory. Thedaysparameter treats1as 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!
| 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) { |
There was a problem hiding this comment.
🎯 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 testsRepository: 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.tsRepository: 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.
| 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"); | ||
| }); |
There was a problem hiding this comment.
🎯 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.
…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.
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_LIMITis 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 listsurfaces aLOWbalance indicator for underfunded accounts.How the checks compose in
runAutoExtensionsValidation
npm ci— clean install, 0 vulnerabilities (npm audit --audit-level=highand--omit=dev)npm run lint— 0 errorsnpm run test -- --coverage— 100 files / 1319 tests passing (includes the 2 new cooldown tests, 4 new min-balance tests, and 2channels listlow-balance tests)npm run build— TypeScript compiles with zero errorspackage-lock.jsonin sync withpackage.jsonMerge guidance for maintainers
feature/min-balance-safety-check) was updated to this same reconciled state, so merging feat(core): add configurable extension cooldown per entry to avoid redundant back-to-back extensions #586 then feat(core): add configurable minimum-balance safety check before extension submission #587 applies only the min-balance delta.Resolves #510 and #504 together.