@opennodes/registry 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/stagec.js +25 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opennodes/registry",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "OpenNodes (ONP) reference registry: node registration with proof-of-resource qualification (Stage A/B/C), transparent ranking, enforceable estimates, catalog importers (OpenRouter, Hugging Face, models.dev, Ollama), MCP surface, and the web app (catalog, chat, advisor, Node Builder)",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/stagec.js CHANGED
@@ -13,12 +13,27 @@ export function startStageC(store, {
13
13
  livenessIntervalMs = 300_000, // spec: <=5 min; tests shorten this
14
14
  blindIntervalMs = 3_600_000,
15
15
  failThreshold = 3,
16
+ recoverThreshold = 3, // consecutive liveness successes that lift a liveness suspension
16
17
  } = {}) {
17
18
  let stopped = false;
18
19
 
20
+ /**
21
+ * A suspension for liveness is temporary: the node is still probed, and after
22
+ * `recoverThreshold` consecutive successes it returns to the tier it held. Suspensions
23
+ * for any other reason (dispute, fraud, operator request) are never lifted here.
24
+ * Returns the tier to restore to, or null when the node is not eligible.
25
+ */
26
+ async function livenessSuspension(node) {
27
+ if (node.state !== 'suspended') return null;
28
+ const last = [...(await store.transitionsFor(node.id))].reverse().find((t) => t.to_state === 'suspended');
29
+ if (!last || !String(last.reason ?? '').startsWith('liveness:')) return null;
30
+ return OBSERVED_STATES.includes(last.from_state) ? last.from_state : 'community';
31
+ }
32
+
19
33
  async function livenessSweep() {
20
34
  for (const node of await store.allNodes()) {
21
- if (!OBSERVED_STATES.includes(node.state)) continue;
35
+ const restoreTo = await livenessSuspension(node);
36
+ if (!OBSERVED_STATES.includes(node.state) && !restoreTo) continue;
22
37
  const card = await store.latestCard(node.id);
23
38
  if (!card) continue;
24
39
  let ok = false;
@@ -37,6 +52,15 @@ export function startStageC(store, {
37
52
  ratio: recent.length ? Math.round((up / recent.length) * 1000) / 1000 : null,
38
53
  });
39
54
 
55
+ if (restoreTo) {
56
+ let consecutiveOk = 0;
57
+ for (const p of recent) { if (!p.ok) break; consecutiveOk++; }
58
+ if (consecutiveOk >= recoverThreshold) {
59
+ await store.setState(node.id, restoreTo, `reinstated: ${consecutiveOk} consecutive liveness successes`);
60
+ await refreshCard?.(node.id); // it may have restarted with a new card revision
61
+ }
62
+ continue;
63
+ }
40
64
  let consecutiveFails = 0;
41
65
  for (const p of recent) { if (p.ok) break; consecutiveFails++; }
42
66
  if (consecutiveFails >= failThreshold) {