@opennodes/registry 0.1.10 → 0.1.11
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.
- package/package.json +1 -1
- package/src/feed.js +42 -0
- package/src/server.js +27 -2
- package/src/web/index.html +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennodes/registry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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/feed.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Feed shaping: a node that keeps dropping offline and coming back would otherwise fill
|
|
2
|
+
// the public feed with suspended/reinstated pairs. Consecutive liveness transitions of
|
|
3
|
+
// one node collapse into a single availability event; everything else passes through.
|
|
4
|
+
|
|
5
|
+
const isLiveness = (e) => /^(liveness:|reinstated:)/.test(e.reason ?? '');
|
|
6
|
+
|
|
7
|
+
/** events: newest first. Returns newest first, with liveness runs (2+ events) collapsed. */
|
|
8
|
+
export function collapseFlaps(events) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const open = new Map(); // node_id -> collapsed entry still accepting older liveness events
|
|
11
|
+
for (const e of events) {
|
|
12
|
+
if (!isLiveness(e)) {
|
|
13
|
+
open.delete(e.node_id);
|
|
14
|
+
out.push(e);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const run = open.get(e.node_id);
|
|
18
|
+
if (run) {
|
|
19
|
+
run.events.push(e);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const entry = { ...e, events: [e] };
|
|
23
|
+
open.set(e.node_id, entry);
|
|
24
|
+
out.push(entry);
|
|
25
|
+
}
|
|
26
|
+
return out.map((entry) => {
|
|
27
|
+
if (!entry.events) return entry;
|
|
28
|
+
const { events: run, ...newest } = entry;
|
|
29
|
+
if (run.length === 1) return newest;
|
|
30
|
+
const oldest = run[run.length - 1];
|
|
31
|
+
const outages = run.filter((e) => e.to === 'suspended').length;
|
|
32
|
+
const now = newest.to === 'suspended' ? 'suspended now' : `${newest.to} again`;
|
|
33
|
+
return {
|
|
34
|
+
...newest,
|
|
35
|
+
from: oldest.from,
|
|
36
|
+
kind: 'availability',
|
|
37
|
+
collapsed: run.length,
|
|
38
|
+
since: oldest.at,
|
|
39
|
+
reason: `availability: ${outages} outage${outages === 1 ? '' : 's'} since ${oldest.at.slice(0, 10)}, ${now}`,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
package/src/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { searchOfferings, searchOfferingsPaged, collectOfferings, RANK_COMPONENT
|
|
|
19
19
|
import { mcpRoutes } from './mcp.js';
|
|
20
20
|
import { hostControlsNamespace, isPrivateHost, makeRateLimiter } from './namespace.js';
|
|
21
21
|
import { exportRoutes } from './export.js';
|
|
22
|
+
import { collapseFlaps } from './feed.js';
|
|
22
23
|
import {
|
|
23
24
|
importModelsDev, importHuggingFace, importOpenRouter,
|
|
24
25
|
importOllamaLibrary, parseOllamaLibrary,
|
|
@@ -321,13 +322,16 @@ export async function startRegistry({
|
|
|
321
322
|
|
|
322
323
|
// Freshness feed: recent listing events (registrations, tier changes, suspensions),
|
|
323
324
|
// newest first, each linked to its provenance reason.
|
|
325
|
+
// Liveness flaps (suspended/reinstated runs) collapse into one availability event;
|
|
326
|
+
// ?raw=1 returns every transition.
|
|
324
327
|
['GET', '/v0/feed', async (req, res, { query }) => {
|
|
325
328
|
const limit = Math.min(Number(query.get('limit') ?? 50), 200);
|
|
326
|
-
const
|
|
329
|
+
const raw = query.get('raw') === '1';
|
|
330
|
+
const events = (await store.recentTransitions(raw ? limit : Math.min(limit * 10, 2000))).map((t) => ({
|
|
327
331
|
at: t.at, node_id: t.node_id, from: t.from_state, to: t.to_state,
|
|
328
332
|
reason: t.reason, source: t.source ?? 'registration',
|
|
329
333
|
}));
|
|
330
|
-
sendJson(res, 200, { events });
|
|
334
|
+
sendJson(res, 200, { events: raw ? events : collapseFlaps(events).slice(0, limit) });
|
|
331
335
|
}],
|
|
332
336
|
|
|
333
337
|
['GET', '/v0/nodes/:id', async (req, res, { params }) => {
|
|
@@ -399,6 +403,27 @@ export async function startRegistry({
|
|
|
399
403
|
sendJson(res, 200, recommend(all, features, body.policy ?? {}));
|
|
400
404
|
}],
|
|
401
405
|
|
|
406
|
+
// Delist (admin): operator opt-out of an imported listing, or retiring a node. Search,
|
|
407
|
+
// the directory and Stage C skip delisted nodes, and re-imports never revive them.
|
|
408
|
+
// Body {reason}. A registered node returns by registering again; relist re-opens imports.
|
|
409
|
+
['POST', '/v0/admin/nodes/:id/delist', async (req, res, { params }) => {
|
|
410
|
+
if (!adminOk(req, res)) return;
|
|
411
|
+
const node = await store.getNode(params.id);
|
|
412
|
+
if (!node) return problem(res, 404, 'unknown-node', params.id);
|
|
413
|
+
const { reason } = await readJson(req);
|
|
414
|
+
if (node.state !== 'delisted') await store.setState(node.id, 'delisted', `delisted: ${reason || 'by registry admin'}`);
|
|
415
|
+
sendJson(res, 200, { id: node.id, state: 'delisted' });
|
|
416
|
+
}],
|
|
417
|
+
['POST', '/v0/admin/nodes/:id/relist', async (req, res, { params }) => {
|
|
418
|
+
if (!adminOk(req, res)) return;
|
|
419
|
+
const node = await store.getNode(params.id);
|
|
420
|
+
if (!node) return problem(res, 404, 'unknown-node', params.id);
|
|
421
|
+
if (node.state !== 'delisted') return problem(res, 409, 'not-delisted', `${node.id} is ${node.state}`);
|
|
422
|
+
if (node.source !== 'import') return problem(res, 409, 'register-again', 'a registered node returns by registering again (POST /v0/nodes)');
|
|
423
|
+
await store.setState(node.id, 'indexed', 'relisted by registry admin');
|
|
424
|
+
sendJson(res, 200, { id: node.id, state: 'indexed' });
|
|
425
|
+
}],
|
|
426
|
+
|
|
402
427
|
// Seed importer (admin): body {catalog} in models.dev shape, or {url} to fetch it.
|
|
403
428
|
['POST', '/v0/import/models-dev', async (req, res) => {
|
|
404
429
|
if (!adminOk(req, res)) return;
|
package/src/web/index.html
CHANGED
|
@@ -317,7 +317,7 @@
|
|
|
317
317
|
<!-- ================= NEW (feed) ================= -->
|
|
318
318
|
<div id="view-feed" hidden>
|
|
319
319
|
<h1>New</h1>
|
|
320
|
-
<p class="sub">Dated listing events, newest first — registrations, tier changes, suspensions — each with its provenance. Also available as <a id="feed-json" href="/v0/feed">JSON</a> for agents.</p>
|
|
320
|
+
<p class="sub">Dated listing events, newest first — registrations, tier changes, suspensions — each with its provenance. A node that keeps dropping offline and coming back shows as one availability line. Also available as <a id="feed-json" href="/v0/feed">JSON</a> for agents (<a href="/v0/feed?raw=1">every transition</a>).</p>
|
|
321
321
|
<div class="panel pad" id="feed-list"></div>
|
|
322
322
|
</div>
|
|
323
323
|
|
|
@@ -1729,7 +1729,7 @@ async function loadFeed() {
|
|
|
1729
1729
|
$('#feed-list').innerHTML = events.length ? events.map((e) => `<div class="feeditem">
|
|
1730
1730
|
<span class="prov">${esc((e.at || '').replace('T', ' ').slice(0, 16))}</span>
|
|
1731
1731
|
<span><a href="#/node/${encodeURIComponent(e.node_id)}" class="mono" style="font-size:0.78rem">${esc(e.node_id)}</a>
|
|
1732
|
-
→ ${tierChip(e.to)} ${e.reason ? '<span style="color:var(--muted)">— ' + esc(e.reason) + '</span>' : ''}</span>
|
|
1732
|
+
→ ${tierChip(e.to)} ${e.reason ? '<span style="color:var(--muted)">— ' + esc(e.reason) + (e.collapsed ? ' (' + e.collapsed + ' events)' : '') + '</span>' : ''}</span>
|
|
1733
1733
|
</div>`).join('') : '<p class="sub">No events yet.</p>';
|
|
1734
1734
|
} catch (e) { $('#feed-list').innerHTML = '<p class="err">' + esc(e.message) + '</p>'; }
|
|
1735
1735
|
}
|