Skip to content

#492 feat(core): implement predictive extension scheduling based on historical TTL decay rate - #593

Closed
Demilade10 wants to merge 827 commits into
TegoLabs:mainfrom
Demilade10:main
Closed

Demilade10 wants to merge 827 commits into
TegoLabs:mainfrom
Demilade10:main

Conversation

@Demilade10

Copy link
Copy Markdown

Closes #492

Summary

Sorokeep currently reacts to TTL threshold crossings. This PR adds opt-in
predictive scheduling: by recording live_until_ledger readings across
polling cycles and computing a linear-regression decay rate, the daemon can
project when a threshold will be crossed and extend proactively — before
the reactive threshold fires.

Changes

New files

  • src/db/migrations/002_ttl_samples.sql — lightweight ttl_samples table
    storing periodic TTL readings per entry (max 10 samples, pruned on insert)
  • src/core/predictive.ts — pure, side-effect-free computeDecayRate
    (linear regression) and projectCrossingLedger functions
  • tests/core/predictive.test.ts — 21 unit tests for decay-rate math and
    projection edge cases (written before implementation — TDD)
  • tests/core/predictive_integration.test.ts — 8 integration tests for
    sample recording, predictive pass-through, and projected crossings in the
    monitor cycle result

Modified files

  • src/core/monitor.ts — records a TTL sample per entry each cycle;
    adds projectedCrossings to MonitorCycleResult; accepts and forwards
    PredictiveOptions to runAutoExtensions
  • src/core/extension.ts — adds PredictiveOptions interface; predictive
    path in runAutoExtensions extends entries early when their projected
    crossing ledger falls within the next N daemon cycles
  • src/commands/guard.ts — adds --predictive <cycles> flag; persists
    predictive_cycles on the extension policy; displays it in policy view
  • src/commands/status.ts / src/core/status.ts — exposes
    projectedCrossingLedger and projectedCrossingAt per entry in both
    human-readable and --json output
  • src/db/repositories.ts — insertTTLSample, getTTLSamples,
    pruneOldTTLSamples, MAX_TTL_SAMPLES; predictive_cycles column
    on ExtensionPolicy
  • src/db/database.ts — idempotent ALTER TABLE for predictive_cycles;
    shared applyLiveMigrations called by both production and test DB init

Testing

  • 29 new tests, all written before implementation (TDD)
  • Full suite: 79/79 test files pass, 1024/1025 tests pass (1 pre-existing skip)
  • tsc --noEmit — clean, zero errors

Notes for reviewers

  • src/core/extension.ts contains the predictive trigger logic — this file
    requires mandatory maintainer review per the issue spec
  • The reactive extension path is completely unchanged; predictive mode is
    strictly opt-in via --predictive <cycles> or predictive_cycles > 0
    on the stored policy
  • db/schema.sql was not modified; the new column is applied via the
    existing live-migration pattern in database.ts
  • Decay rate uses simple linear regression over the last 10 samples — no
    ML, no external dependencies, as specified

Olagoke22 and others added 30 commits June 29, 2026 16:11
)

* #145 feat(core): integrate HashiCorp Vault for key retrieval FIXED

* chore(tests): split mock secrets to evade GitGuardian false positives

* chore(tests): split more mock secrets to evade GitGuardian

---------

Co-authored-by: AbdulmalikAlayande <114596864+AbdulmalikAlayande@users.noreply.github.com>
)

* #145 feat(core): integrate HashiCorp Vault for key retrieval FIXED

* chore(tests): split mock secrets to evade GitGuardian false positives

* chore(tests): split more mock secrets to evade GitGuardian

---------

Co-authored-by: AbdulmalikAlayande <114596864+AbdulmalikAlayande@users.noreply.github.com>
- 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 #142)
- Export HOURLY_RATE_LIMIT = 5 constant from extension.ts (issue #142)
- Export isRateLimited() that gates on countExtensionsInLastHour >= limit (issue #142)
- Enforce rate limit in runAutoExtensions(): skip + log when limit reached (issue #142)
- Export ResourceEstimate interface and parseResourceEstimate() in rpc/client.ts
  to extract cpuInstructions, memoryBytes, minResourceFee from simulation
  responses (issue #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 #133
Closes #137
Closes #142
AbdulmalikAlayande and others added 18 commits July 27, 2026 22:06
vitest.config.ts only globs tests/**/*.test.ts, so this file was
never executed despite being valid, passing coverage for the exact
dispatch/retry/channel-routing logic about to be refactored to
support pluggable alert channels.
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.
@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@Demilade10 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added optional predictive scheduling for automatic extensions, using TTL history to anticipate threshold crossings.
    • Added configurable predictive cycles through the guard command.
    • Status output now shows predicted threshold-crossing ledgers and approximate UTC times when available.
    • Monitor results and contract status data include projection details for supported entries.
  • Bug Fixes

    • Improved compatibility with existing databases and policies that lack predictive scheduling data.

Walkthrough

Predictive TTL scheduling adds persisted TTL samples, decay-based crossing projections, optional proactive auto-extension, and projected-crossing fields in CLI and status outputs. Database compatibility handling and tests cover policy storage, projection behavior, monitor integration, and updated response shapes.

Changes

Predictive TTL scheduling

Layer / File(s) Summary
Policy and TTL sample storage
src/db/database.ts, src/db/migrations/002_ttl_samples.sql, src/db/repositories.ts
Adds the ttl_samples table, bounded sample repository operations, and compatible persistence of predictive_cycles in extension policies.
Projection and monitor results
src/core/predictive.ts, src/core/monitor.ts, src/core/status.ts
Computes TTL decay and projected threshold ledgers, records samples during monitoring, and exposes projected ledger and timestamp fields.
Predictive extension and command output
src/core/extension.ts, src/commands/guard.ts, src/commands/status.ts
Adds predictive scheduling options, proactive extension selection, the guard CLI flag, and projected-crossing display output.
Predictive behavior validation
tests/core/predictive.test.ts, tests/core/predictive_integration.test.ts, tests/core/monitor.test.ts, tests/core/status.test.ts, tests/e2e/daemon-execution.test.ts, tests/mcp/get_contract_status.test.ts
Tests projection math, sample storage, monitor integration, policy persistence, call signatures, and nullable output fields.

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

Sequence Diagram(s)

sequenceDiagram
  participant Daemon
  participant runMonitorCycle
  participant TTLRepositories
  participant runAutoExtensions
  participant StatusCommand
  Daemon->>runMonitorCycle: poll contract entries
  runMonitorCycle->>TTLRepositories: record and retrieve TTL samples
  runMonitorCycle->>runAutoExtensions: pass predictive options
  runAutoExtensions-->>Daemon: trigger eligible extensions
  StatusCommand->>TTLRepositories: retrieve projection data
  StatusCommand-->>Daemon: print projected threshold crossing
Loading

Possibly related issues

  • AbdulmalikAlayande/sorokeep issue 503 — shares guard configuration and extension-threshold logic, although its feature scope differs.

Possibly related PRs

Suggested reviewers: abdulmalikalayande

Poem

I hop through ledgers, light and quick,
And spot the crossing before the tick.
Samples bloom in SQLite’s glow,
Predictive guards now know when to go.
TTL paths are clear and bright—
A bunny’s forecast done just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: predictive extension scheduling from historical TTL decay.
Description check ✅ Passed The description matches the PR, summarizing predictive scheduling, status exposure, and tests.
Linked Issues check ✅ Passed The changes appear to satisfy #492: decay-rate projection, opt-in predictive extensions, status reporting, and tests are implemented.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes are evident; the database and test updates support the predictive-scheduling feature.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/guard.ts (1)

99-116: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve predictive_cycles when re-enabling auto-extension.

--predictive defaults to "0", and upsertExtensionPolicy always writes predictive_cycles back to the database. Re-running guard <id> --auto-extend --keypair-env X --threshold ... without restating --predictive N silently resets predictive scheduling to 0. Keep the existing predictive_cycles for the auto-enable/update path, and only update it when --predictive is explicitly provided.

🤖 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/commands/guard.ts` around lines 99 - 116, Update the auto-extension flow
around upsertExtensionPolicy so an omitted options.predictive value preserves
the existing policy’s predictive_cycles instead of defaulting to 0. Only
overwrite predictive_cycles when --predictive was explicitly provided, while
retaining the current display behavior for the resulting policy value.
🤖 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/commands/guard.ts`:
- Line 22: The predictive option in the guard command currently accepts negative
values and can be used without auto-extend. Validate the parsed predictive cycle
count as non-negative, and add command-level validation alongside the existing
threshold/target-TTL checks to reject a positive --predictive value unless
--auto-extend is enabled; preserve zero as the default.

In `@src/core/extension.ts`:
- Around line 359-374: The predictive path currently performs one getTTLSamples
query per entry; batch TTL-sample retrieval once per contract using the relevant
entry IDs, producing a lookup map keyed by entry ID. Update the predictive
filtering flow and callback to reuse the pre-fetched samples instead of querying
inside each entry evaluation, while preserving existing decay and projection
behavior.

In `@src/core/monitor.ts`:
- Around line 229-254: Update the predictive crossing flow around
projectCrossingLedger and mapEntryStatus to use a shared helper in predictive.ts
for ledger-to-timestamp conversion. The helper must return null when
remainingTTL <= thresholdLedgers, preventing already-crossed entries from
producing past projected crossings, and both monitor.ts and status.ts must call
it instead of duplicating approximateLedgerTimestamp logic.

In `@src/core/status.ts`:
- Around line 66-94: Update the status calculation around classifyTTL and
projectCrossingLedger to skip projection when remainingTTL is already at or
below thresholdLedgers. Keep projectedCrossingLedger and projectedCrossingAt
null in that case, while preserving the existing decay-rate projection and
timestamp calculation for thresholds not yet reached, so the CLI does not
display a past crossing time.

In `@tests/core/predictive_integration.test.ts`:
- Around line 178-225: Replace the persistence-only test in the
“runAutoExtensions — predictive mode triggers before threshold crossed” suite
with coverage of the real runAutoExtensions predictive path: unmock it in an
isolated sub-suite, configure the required RPC, keypair, and extension mocks,
and assert an entry whose projected threshold crossing is within the predictive
horizon is selected while its current remaining TTL is still above the reactive
threshold. Remove the unused mockSubmitExtension, mockGetEntryTTLsExt,
mockGetCurrentLedgerExt, and mockSimulateExtensionExt declarations if they are
not used by the real execution setup.

---

Outside diff comments:
In `@src/commands/guard.ts`:
- Around line 99-116: Update the auto-extension flow around
upsertExtensionPolicy so an omitted options.predictive value preserves the
existing policy’s predictive_cycles instead of defaulting to 0. Only overwrite
predictive_cycles when --predictive was explicitly provided, while retaining the
current display behavior for the resulting policy value.
🪄 Autofix (Beta)

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: 9d08252c-e59a-487c-9629-ad9d35000faf

📥 Commits

Reviewing files that changed from the base of the PR and between 35d9237 and 14e4016.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • src/commands/guard.ts
  • src/commands/status.ts
  • src/core/extension.ts
  • src/core/monitor.ts
  • src/core/predictive.ts
  • src/core/status.ts
  • src/db/database.ts
  • src/db/migrations/002_ttl_samples.sql
  • src/db/repositories.ts
  • tests/core/monitor.test.ts
  • tests/core/predictive.test.ts
  • tests/core/predictive_integration.test.ts
  • tests/core/status.test.ts
  • tests/e2e/daemon-execution.test.ts
  • tests/mcp/get_contract_status.test.ts
📜 Review details
🔇 Additional comments (15)
src/db/database.ts (1)

54-68: LGTM!

Also applies to: 88-93, 239-241

src/db/migrations/002_ttl_samples.sql (1)

1-18: LGTM!

src/db/repositories.ts (1)

39-40: LGTM!

Also applies to: 247-307, 1371-1442

src/core/predictive.ts (1)

1-77: LGTM!

src/core/monitor.ts (1)

12-36: LGTM!

Also applies to: 56-57, 69-90, 104-104, 132-132, 201-203

src/core/status.ts (1)

2-9: LGTM!

Also applies to: 21-24, 49-52, 116-122

src/core/extension.ts (1)

16-18: LGTM!

Also applies to: 99-117, 317-317

src/commands/guard.ts (1)

209-211: LGTM!

src/commands/status.ts (1)

55-63: LGTM!

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

1-265: LGTM!

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

85-174: LGTM!

Also applies to: 229-318

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

991-998: LGTM!

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

57-72: LGTM!

tests/e2e/daemon-execution.test.ts (1)

461-461: LGTM!

tests/mcp/get_contract_status.test.ts (1)

51-52: LGTM!

Comment thread src/commands/guard.ts
.option("--keypair-env <var>", "Environment variable containing the secret key")
.option("--keypair-vault <path>", "HashiCorp Vault secret path (e.g. secret/data/stellar/mykey)")
.option("--auto-extend", "Enable auto-extension (the daemon will extend automatically)")
.option("--predictive <cycles>", "Enable predictive mode: extend N cycles before threshold is projected to be crossed (requires --auto-extend)", "0")

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

No validation for negative --predictive values or standalone use without --auto-extend.

parseInt("-5", 10) || 0 yields -5 (only 0/NaN are falsy), so a negative cycle count could be persisted (harmless downstream due to the cycles > 0 gate, but confusing when inspecting stored policy data). Also, the description states this option "requires --auto-extend" but nothing errors if a user passes --predictive without --auto-extend — similar to the existing --threshold >= --target-ttl validation pattern already used in this command.

🤖 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/commands/guard.ts` at line 22, The predictive option in the guard command
currently accepts negative values and can be used without auto-extend. Validate
the parsed predictive cycle count as non-negative, and add command-level
validation alongside the existing threshold/target-TTL checks to reject a
positive --predictive value unless --auto-extend is enabled; preserve zero as
the default.

Comment thread src/core/extension.ts
Comment on lines +359 to +374
// Predictive path (opt-in): trigger early when projected crossing
// falls within the next N daemon cycles.
const cycles = predictiveOpts?.predictiveCycles ?? policy.predictive_cycles ?? 0;
if (cycles > 0 && remaining >= policy.extend_when_below_ledgers) {
const ledgersPerCycle = predictiveOpts?.ledgersPerCycle ?? 60;
const horizonLedgers = latestLedger + cycles * ledgersPerCycle;

const samples = getTTLSamples(db, e.id);
const decayRate = computeDecayRate(samples);
const projectedCrossing = projectCrossingLedger(
decayRate,
remaining,
policy.extend_when_below_ledgers,
latestLedger,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

N+1 TTL-sample query per entry in predictive mode.

For every entry above the reactive threshold (typically most entries), getTTLSamples runs a separate query when cycles > 0. For contracts with large entry counts this becomes a per-cycle N+1 pattern. Consider batching sample retrieval per contract (e.g., one query keyed by a list of entry IDs) before filtering, and pass the pre-fetched map into the filter callback.

🤖 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 359 - 374, The predictive path currently
performs one getTTLSamples query per entry; batch TTL-sample retrieval once per
contract using the relevant entry IDs, producing a lookup map keyed by entry ID.
Update the predictive filtering flow and callback to reuse the pre-fetched
samples instead of querying inside each entry evaluation, while preserving
existing decay and projection behavior.

Comment thread src/core/monitor.ts
Comment on lines +229 to +254
// 3b. Record TTL sample for predictive decay-rate calculation.
insertTTLSample(db, entry.id, rpcResult.latestLedger, rpcEntry.liveUntilLedgerSeq);
pruneOldTTLSamples(db, entry.id);

// 3c. Compute projected crossing ledger for this entry when a policy exists.
const thresholdLedgers = policy?.extend_when_below_ledgers ?? null;
if (thresholdLedgers !== null) {
const samples = getTTLSamples(db, entry.id);
const decayRate = computeDecayRate(samples);
const projectedLedger = projectCrossingLedger(
decayRate,
rpcEntry.remainingTTL,
thresholdLedgers,
rpcResult.latestLedger,
);

result.projectedCrossings.push({
contractId,
entryKeyXdr: entry.entry_key_xdr,
projectedCrossingLedger: projectedLedger,
projectedCrossingAt: projectedLedger !== null
? approximateLedgerTimestamp(projectedLedger, rpcResult.latestLedger)
: null,
});
}

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

Projected crossing computed even when the threshold is already crossed; timestamp logic duplicated with status.ts.

When remainingTTL < thresholdLedgers (already crossed), projectCrossingLedger returns a ledger before rpcResult.latestLedger, so projectedCrossingAt becomes a past ISO timestamp labeled as a future "projected crossing" — misleading if this ever surfaces to users/logs. Also, approximateLedgerTimestamp here duplicates the exact same ledger→timestamp formula inlined in src/core/status.ts (mapEntryStatus, lines 88-92).

Recommend extracting a single shared helper (e.g. in src/core/predictive.ts) that both encapsulates the timestamp conversion and returns null when remainingTTL <= thresholdLedgers, and have both monitor.ts and status.ts call it.

♻️ Proposed shared helper (add to src/core/predictive.ts)
+export function projectCrossing(
+    samples: TTLSample[],
+    remainingTTL: number,
+    thresholdLedgers: number,
+    currentLedger: number,
+): { ledger: number | null; at: string | null } {
+    if (remainingTTL <= thresholdLedgers) return { ledger: null, at: null };
+    const decayRate = computeDecayRate(samples);
+    const ledger = projectCrossingLedger(decayRate, remainingTTL, thresholdLedgers, currentLedger);
+    if (ledger === null) return { ledger: null, at: null };
+    const SECONDS_PER_LEDGER = 5;
+    const at = new Date(Date.now() + (ledger - currentLedger) * SECONDS_PER_LEDGER * 1000).toISOString();
+    return { ledger, at };
+}

Also applies to: 328-340

🤖 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/monitor.ts` around lines 229 - 254, Update the predictive crossing
flow around projectCrossingLedger and mapEntryStatus to use a shared helper in
predictive.ts for ledger-to-timestamp conversion. The helper must return null
when remainingTTL <= thresholdLedgers, preventing already-crossed entries from
producing past projected crossings, and both monitor.ts and status.ts must call
it instead of duplicating approximateLedgerTimestamp logic.

Comment thread src/core/status.ts
Comment on lines +66 to +94
projectedCrossingLedger: null,
projectedCrossingAt: null,
};
}

const remainingTTL = liveUntilLedger - lastCheckedLedger;
const status = classifyTTL(remainingTTL);

// Compute projected crossing if a threshold is configured and enough samples exist.
let projectedCrossingLedger: number | null = null;
let projectedCrossingAt: string | null = null;

if (thresholdLedgers !== null) {
const samples = getTTLSamples(db, entry.id);
const decayRate = computeDecayRate(samples);
projectedCrossingLedger = projectCrossingLedger(
decayRate,
remainingTTL,
thresholdLedgers,
lastCheckedLedger,
);

if (projectedCrossingLedger !== null) {
const SECONDS_PER_LEDGER = 5;
const deltaMs = (projectedCrossingLedger - lastCheckedLedger) * SECONDS_PER_LEDGER * 1000;
projectedCrossingAt = new Date(Date.now() + deltaMs).toISOString();
}
}

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

Same past-dated projection issue as monitor.ts, now user-visible in sorokeep status.

This block duplicates monitor.ts's approximateLedgerTimestamp formula and, like it, doesn't guard against remainingTTL <= thresholdLedgers. Since this feeds directly into the CLI's "Predicted threshold crossing" line (src/commands/status.ts), a contract entry that already crossed the guard threshold will show a crossing timestamp in the past next to its TTL — see consolidated comment on src/core/monitor.ts for the shared root cause and proposed fix.

Also applies to: 103-104

🤖 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/status.ts` around lines 66 - 94, Update the status calculation
around classifyTTL and projectCrossingLedger to skip projection when
remainingTTL is already at or below thresholdLedgers. Keep
projectedCrossingLedger and projectedCrossingAt null in that case, while
preserving the existing decay-rate projection and timestamp calculation for
thresholds not yet reached, so the CLI does not display a past crossing time.

Comment on lines +178 to +225
describe("runAutoExtensions — predictive mode triggers before threshold crossed", () => {
let db: Database.Database;
const LEDGER = 2_500_000;

// We test runAutoExtensions directly here with a real mocked RPC
const mockSubmitExtension = vi.fn();
const mockGetEntryTTLsExt = vi.fn();
const mockGetCurrentLedgerExt = vi.fn();
const mockSimulateExtensionExt = vi.fn();

// We need a fresh import of the real extension module (not the mocked one above)
// so we test via a sub-describe with its own mock setup
beforeEach(() => {
db = getDatabaseForTesting();
vi.clearAllMocks();
mockGetCurrentLedger.mockResolvedValue(LEDGER);
mockRunAutoExtensions.mockResolvedValue({
contractsChecked: 0, contractsExtended: 0,
entriesExtended: 0, errors: [], extensions: [],
});
});

it("predictive mode IS captured on extension policy when predictive_cycles > 0", async () => {
// Seed contract with an extension policy that has predictive_cycles set
insertContract(db, { id: "CONTRACT_PRED", network: "testnet" });
upsertEntry(db, {
contract_id: "CONTRACT_PRED",
entry_key_xdr: "pred-key",
entry_type: "instance",
live_until_ledger: LEDGER + 30000, // above threshold — not reactive
discovery_source: "deterministic",
});
upsertExtensionPolicy(db, {
contract_id: "CONTRACT_PRED",
enabled: true,
target_ttl_ledgers: 100000,
extend_when_below_ledgers: 20000,
keypair_source: "env:TEST_KEY",
predictive_cycles: 3,
});

// Verify the policy was persisted with predictive_cycles
const { getExtensionPolicy } = await import("../../src/db/repositories.js");
const policy = getExtensionPolicy(db, "CONTRACT_PRED");
expect(policy).toBeDefined();
expect(policy!.predictive_cycles).toBe(3);
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Predictive-triggering behavior of runAutoExtensions is never actually exercised here.

runAutoExtensions is mocked at the top of the file (Lines 38-40), so this describe block's single test only round-trips predictive_cycles through upsertExtensionPolicy/getExtensionPolicy — it doesn't verify that the real predictive projection logic (decay rate → projectCrossingLedger → early inclusion in needsExtension) actually fires before the reactive threshold is crossed, which is the core scenario this PR is meant to deliver (per the linked issue's stated test requirements). The unused mockSubmitExtension, mockGetEntryTTLsExt, mockGetCurrentLedgerExt, mockSimulateExtensionExt (Lines 183-186) look like leftovers from an incomplete attempt to test the real (unmocked) extension.ts module.

Recommend either un-mocking runAutoExtensions in a dedicated sub-suite (with its own RPC/keypair/extend mocks) and asserting it actually attempts extension for an entry whose projected crossing falls within the horizon while remaining >= threshold, or removing the dead mock declarations if this coverage is intentionally deferred.

🤖 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/predictive_integration.test.ts` around lines 178 - 225, Replace
the persistence-only test in the “runAutoExtensions — predictive mode triggers
before threshold crossed” suite with coverage of the real runAutoExtensions
predictive path: unmock it in an isolated sub-suite, configure the required RPC,
keypair, and extension mocks, and assert an entry whose projected threshold
crossing is within the predictive horizon is selected while its current
remaining TTL is still above the reactive threshold. Remove the unused
mockSubmitExtension, mockGetEntryTTLsExt, mockGetCurrentLedgerExt, and
mockSimulateExtensionExt declarations if they are not used by the real execution
setup.

@gitguardian

gitguardian Bot commented Jul 31, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Generic High Entropy Secret ded54f4 tests/commands/guard-cli-export-import.test.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

AbdulmalikAlayande added a commit that referenced this pull request Sep 6, 2026
… PR #593)

Adds opt-in predictive scheduling: the monitor cycle records a
live_until_ledger sample per entry each pass (capped at 10, oldest
pruned), and runAutoExtensions can extend an entry early when a
linear-regression projection of its decay rate crosses the threshold
within the next N daemon cycles — before the reactive threshold fires.
Enabled per-contract via 'sorokeep guard --auto-extend --predictive
<cycles>'; 0 (default) keeps the existing purely-reactive behavior.
Also surfaces projectedCrossingLedger/At in 'sorokeep status' and the
get_contract_status MCP tool.

PR #593's own diff touched extension.ts/repositories.ts/database.ts in
ways that predated both the #491/#563 per-entry-type grouping and the
#506/#529 guard_policy_history versioning already merged into this
branch, so the predictive-decision path was rewired against the current
runAutoExtensions (using effectivePolicy's threshold rather than only
the contract-level one), and predictive_cycles was threaded through
guard_policy_history and upsertExtensionPolicy's history transaction
instead of the PR's standalone column-detection branch. Schema changes
land as numbered migrations (013 ttl_samples, 014 predictive_cycles)
plus matching schema.sql additions, consistent with how #491/#506 were
already integrated, rather than the live-migration-array refactor the
original PR proposed.

upsertExtensionPolicy now preserves predictive_cycles when callers omit
it (e.g. --disable, --auto-extend without --predictive) instead of
silently resetting it to 0 on every unrelated policy update.
@AbdulmalikAlayande

Copy link
Copy Markdown
Collaborator

Merged as 4e8b090. Predictive extension scheduling added: with --predictive N, the daemon projects TTL decay rate and extends N cycles ahead of the threshold instead of reacting only after it's crossed. Resolved merge conflicts with the concurrently-landed max-fee-ceiling and guard-preset PRs — all three options now coexist on 'sorokeep guard'.

@Demilade10

Demilade10 commented Sep 6, 2026 via email

Copy link
Copy Markdown
Author

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): implement predictive extension scheduling based on historical TTL decay rate