autoclean: don't increment num_cleaned when record wasn't even a candidate.
What changed, and why it matters
This commit fixes a bookkeeping bug in the autoclean-once feature. Previously, when you asked to clean up failed payment forwards, the tool counted every forward that wasn't deleted—including successful forwards that were never supposed to be deleted—as 'uncleaned'. Now it only counts records that were actually candidates for cleanup but were too new to be removed. This is a user-interface/reporting fix, not a security vulnerability.
No security action required. This is a normal bug fix improving API accuracy. Operators relying on autoclean-once reporting may update to v25.12 for clearer metrics.
Security signals we found
No security-relevant code path altered
Fix is purely informational/statistical in RPC response
No memory safety, authentication, authorization, or cryptographic changes
No CVE or security advisory referenced in commit
Evidence from the diff
The patch modifies plugins/autoclean.c so that subsystem->num_uncleaned is no longer incremented when a record has no matching variant (e.g., a non-failed forward when the subsystem is failedforwards) or when the configured age is zero (meaning the variant is not configured for cleaning). It also updates JSON schemas and tests to reflect that uncleaned now means ‘candidate records too new to delete’ rather than ‘all records not deleted’.
Changed components
plugins/autoclean.cdoc/schemas/autoclean-once.jsoncontrib/msggen/msggen/schema.jsontests/test_plugin.pyInspect captured patch +33 / −31
diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json
index f0c83e0f..adf92a92 100644
--- a/contrib/msggen/msggen/schema.json
+++ b/contrib/msggen/msggen/schema.json
@@ -1862,7 +1862,9 @@
" * `succeededpays`: payment attempts which succeeded (`complete` in listpays `status`).",
" * `expiredinvoices`: invoices which were not paid (and cannot be) (`expired` in listinvoices `status`).",
" * `paidinvoices`: invoices which were paid (`paid` in listinvoices `status).",
- " * `networkevents`: all events in listnetworkevents (added *v25.12*)"
+ " * `networkevents`: all events in listnetworkevents (added *v25.12*)",
+ "",
+ "NOTE: until v25.12, the `uncleaned` field contained all entries not removed (e.g. in `failedforwards` it counted all forwards, not just failed ones). This was an interface only an engineer could love, so it was fixed."
]
},
"age": {
@@ -1894,13 +1896,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of successful forwards deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of successful forwards *not* deleted (too new)."
]
}
}
@@ -1916,13 +1918,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of failed forwards deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of failed forwards *not* deleted (too new)."
]
}
}
@@ -1938,13 +1940,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of successful payments deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of successful forwards *not* deleted (too new)."
]
}
}
@@ -1960,13 +1962,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of unsuccessful payments deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of unsuccessful payments *not* deleted (too new)."
]
}
}
@@ -1982,13 +1984,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of paid invoices deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of paid invoices *not* deleted (too new)."
]
}
}
@@ -2004,13 +2006,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of expired invoices deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of expired invoices *not* deleted (too new)."
]
}
}
diff --git a/doc/schemas/autoclean-once.json b/doc/schemas/autoclean-once.json
index d23c6221..f10149fb 100644
--- a/doc/schemas/autoclean-once.json
+++ b/doc/schemas/autoclean-once.json
@@ -32,7 +32,9 @@
" * `succeededpays`: payment attempts which succeeded (`complete` in listpays `status`).",
" * `expiredinvoices`: invoices which were not paid (and cannot be) (`expired` in listinvoices `status`).",
" * `paidinvoices`: invoices which were paid (`paid` in listinvoices `status).",
- " * `networkevents`: all events in listnetworkevents (added *v25.12*)"
+ " * `networkevents`: all events in listnetworkevents (added *v25.12*)",
+ "",
+ "NOTE: until v25.12, the `uncleaned` field contained all entries not removed (e.g. in `failedforwards` it counted all forwards, not just failed ones). This was an interface only an engineer could love, so it was fixed."
]
},
"age": {
@@ -64,13 +66,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of successful forwards deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of successful forwards *not* deleted (too new)."
]
}
}
@@ -86,13 +88,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of failed forwards deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of failed forwards *not* deleted (too new)."
]
}
}
@@ -108,13 +110,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of successful payments deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of successful forwards *not* deleted (too new)."
]
}
}
@@ -130,13 +132,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of unsuccessful payments deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of unsuccessful payments *not* deleted (too new)."
]
}
}
@@ -152,13 +154,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of paid invoices deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of paid invoices *not* deleted (too new)."
]
}
}
@@ -174,13 +176,13 @@
"cleaned": {
"type": "u64",
"description": [
- "Total number of deletions done this run."
+ "The number of expired invoices deleted."
]
},
"uncleaned": {
"type": "u64",
"description": [
- "The total number of entries *not* deleted this run."
+ "The number of expired invoices *not* deleted (too new)."
]
}
}
diff --git a/plugins/autoclean.c b/plugins/autoclean.c
index 345aa79b..e4601cb0 100644
--- a/plugins/autoclean.c
+++ b/plugins/autoclean.c
@@ -555,13 +555,11 @@ static struct command_result *list_done(struct command *cmd,
variant = ops->get_variant(buf, t, subsystem, ×tamp);
if (!variant) {
- subsystem->num_uncleaned++;
continue;
}
/* Continue if we don't care. */
if (variant->age == 0) {
- subsystem->num_uncleaned++;
continue;
}
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 50117aab..3f26208e 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -3124,7 +3124,7 @@ def test_autoclean_once(node_factory):
# Make sure > 1 second old!
time.sleep(2)
assert (l1.rpc.autoclean_once('failedpays', 1)
- == {'autoclean': {'failedpays': {'cleaned': 1, 'uncleaned': 1}}})
+ == {'autoclean': {'failedpays': {'cleaned': 1, 'uncleaned': 0}}})
assert (l1.rpc.autoclean_once('succeededpays', 1)
== {'autoclean': {'succeededpays': {'cleaned': 1, 'uncleaned': 0}}})
@@ -3133,7 +3133,7 @@ def test_autoclean_once(node_factory):
assert l1.rpc.autoclean_status() == expected[0]
assert (l2.rpc.autoclean_once('failedforwards', 1)
- == {'autoclean': {'failedforwards': {'cleaned': 1, 'uncleaned': 1}}})
+ == {'autoclean': {'failedforwards': {'cleaned': 1, 'uncleaned': 0}}})
expected[1]['autoclean']['failedforwards']['cleaned'] = 1
assert l2.rpc.autoclean_status() == expected[1]
@@ -3143,7 +3143,7 @@ def test_autoclean_once(node_factory):
assert l2.rpc.autoclean_status() == expected[1]
assert (l3.rpc.autoclean_once('expiredinvoices', 1)
- == {'autoclean': {'expiredinvoices': {'cleaned': 1, 'uncleaned': 1}}})
+ == {'autoclean': {'expiredinvoices': {'cleaned': 1, 'uncleaned': 0}}})
expected[2]['autoclean']['expiredinvoices']['cleaned'] = 1
assert l3.rpc.autoclean_status() == expected[2]
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.