OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / docs / vnext / salvage-bundles / 2026-07-13-main-dirty / tracked.patch

828 lines · patch ref:
diff --git a/dashboard/calculator.html b/dashboard/calculator.html
index 3b9cb3c3..05347b1c 100644
--- a/dashboard/calculator.html
+++ b/dashboard/calculator.html
@@ -165,6 +165,7 @@ input[type="number"]:focus{border-color:var(--cyan)}
   </a>
   <nav class="topbar-nav">
     <a href="/dashboard">Dashboard</a>
+    <a href="/catalog">Catalog</a>
     <a href="/payments">Payments</a>
     <a href="/calculator" class="active">Calculator</a>
     <a href="/network">Network</a>
diff --git a/dashboard/catalog.html b/dashboard/catalog.html
index 77aad962..8df14e04 100644
--- a/dashboard/catalog.html
+++ b/dashboard/catalog.html
@@ -139,10 +139,10 @@ body{background:var(--bg) radial-gradient(1100px 500px at 50% -150px,#131a2b,tra
   <nav class="topbar-nav">
     <a href="/dashboard">Dashboard</a>
     <a href="/catalog" class="active">Catalog</a>
-    <a href="/network">Network</a>
-    <a href="/leaderboard">Leaderboard</a>
     <a href="/payments">Payments</a>
     <a href="/calculator">Calculator</a>
+    <a href="/network">Network</a>
+    <a href="/leaderboard">Leaderboard</a>
     <a href="/docs">Docs</a>
   </nav>
 </div>
diff --git a/dashboard/leaderboard.html b/dashboard/leaderboard.html
index 81f07db4..7b1715ff 100644
--- a/dashboard/leaderboard.html
+++ b/dashboard/leaderboard.html
@@ -183,9 +183,10 @@ tr.this-node:hover{background:rgba(88,166,255,.08)}
   </a>
   <nav class="topbar-nav">
     <a href="/dashboard">Dashboard</a>
-    <a href="/network">Network</a>
+    <a href="/catalog">Catalog</a>
     <a href="/payments">Payments</a>
     <a href="/calculator">Calculator</a>
+    <a href="/network">Network</a>
     <a href="/leaderboard" class="active">Leaderboard</a>
     <a href="/docs">Docs</a>
   </nav>
diff --git a/dashboard/payments.html b/dashboard/payments.html
index 15cb5b94..f2e01267 100644
--- a/dashboard/payments.html
+++ b/dashboard/payments.html
@@ -137,6 +137,7 @@ tr:hover td{background:rgba(88,166,255,.03)}
   </a>
   <nav class="topbar-nav">
     <a href="/dashboard">Dashboard</a>
+    <a href="/catalog">Catalog</a>
     <a href="/payments" class="active">Payments</a>
     <a href="/calculator">Calculator</a>
     <a href="/network">Network</a>
@@ -540,20 +541,50 @@ async function fetchPricing() {
 }
 
 /* ===== Fetch wallets ===== */
+function renderWalletsTable(wallets) {
+  var tbody = document.getElementById('walletsBody');
+  clearNode(tbody);
+  if (!wallets || wallets.length === 0) {
+    tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No wallets yet. Apps get 1,000 free credits on first connect. Everything works on the free tier.</td></tr>';
+    return;
+  }
+  for (var i = 0; i < wallets.length; i++) {
+    var w = wallets[i];
+    var row = appendEl(tbody, 'tr');
+    appendCell(row, truncateKey(w.appPubkey));
+    appendCell(row, formatSats(w.balance));
+    appendCell(row, formatSats(w.totalDeposited));
+    appendCell(row, formatSats(w.totalSpent));
+    appendCell(row, formatSats(w.totalBonusReceived + w.welcomeCreditsReceived));
+    var tierCell = appendCell(row, '');
+    tierCell.innerHTML = tierBadge(w.tier);
+    appendCell(row, timeAgo(w.lastActivity));
+  }
+}
+
 async function fetchWallets() {
   try {
     var res = await fetchWithTimeout('/api/v1/credits/stats', { headers: { 'x-api-key': '' } });
-    // Stats endpoint may require auth - fall back gracefully
-    if (!res.ok) return;
-    var stats = await res.json();
-    if (stats.credits) {
-      document.getElementById('walletSummary').textContent =
-        metricCount(stats.credits.totalWallets) + ' wallets, ' + formatSats(stats.credits.totalDeposited) + ' deposited, ' +
-        formatSats(stats.credits.totalSpent) + ' spent';
+    if (res.ok) {
+      var stats = await res.json();
+      if (stats.credits) {
+        document.getElementById('walletSummary').textContent =
+          metricCount(stats.credits.totalWallets) + ' wallets, ' + formatSats(stats.credits.totalDeposited) + ' deposited, ' +
+          formatSats(stats.credits.totalSpent) + ' spent';
+      }
     }
   } catch (e) {
     // Stats may require auth — that's fine
   }
+  try {
+    var wres = await fetchWithTimeout('/api/v1/credits/wallets', { headers: { 'x-api-key': '' } });
+    if (wres.ok) {
+      var wdata = await wres.json();
+      renderWalletsTable(wdata.wallets);
+    }
+  } catch (e) {
+    // Wallet list may require auth — that's fine
+  }
 }
 
 /* ===== Data Fetching & WebSocket ===== */
diff --git a/docs/RELAYKERNEL-GATEWAY-COMPATIBILITY.md b/docs/RELAYKERNEL-GATEWAY-COMPATIBILITY.md
index 069ab529..afd70954 100644
--- a/docs/RELAYKERNEL-GATEWAY-COMPATIBILITY.md
+++ b/docs/RELAYKERNEL-GATEWAY-COMPATIBILITY.md
@@ -41,3 +41,16 @@ compatibility floor.
 
 This is deliberately not a full RelayKernel replacement claim. It is the
 compatibility floor for the current HiveRelay/Blindspark release line.
+
+## Deferred public HTTPS extension
+
+The compatibility floor above preserves today's path-addressed gateway. The
+deferred design for key-derived HTTPS subdomains, publisher domains, signed
+gateway advertisements and leases, exact-byte serving, and proof-carrying HTTP
+responses is specified in
+[`PUBLIC-HTTPS-HIVE-GATEWAY-SPEC.md`](PUBLIC-HTTPS-HIVE-GATEWAY-SPEC.md).
+
+That work remains a T1/public availability layer above RelayKernel. It MUST NOT
+be mounted or advertised by the T2 blind-custody role. Runtime implementation
+starts only after the blind-substrate role, storage-class, and signing contracts
+pass the readiness gates in that specification.
diff --git a/examples/node-app/index.js b/examples/node-app/index.js
index eaf22c95..b92178ff 100644
--- a/examples/node-app/index.js
+++ b/examples/node-app/index.js
@@ -52,8 +52,10 @@ const apps = relay.getAvailableApps()
 if (apps.length > 0) {
   console.log(`\n${apps.length} app row(s) across connected relays:`)
   for (const app of apps) {
-    const id = app.appId || app.appKey.slice(0, 12) + '...'
-    console.log(`  - ${id} (from relay ${app.source.relayPubkey.slice(0, 12)})`)
+    const appKey = app.appKey || app.key || ''
+    const id = app.appId || (appKey ? appKey.slice(0, 12) + '...' : 'unknown')
+    const relayPubkey = app.source && app.source.relayPubkey ? app.source.relayPubkey.slice(0, 12) : 'unknown'
+    console.log(`  - ${id} (from relay ${relayPubkey})`)
   }
 }
 
diff --git a/examples/pear-app/index.js b/examples/pear-app/index.js
index c5aa93f7..6af957e9 100644
--- a/examples/pear-app/index.js
+++ b/examples/pear-app/index.js
@@ -68,8 +68,10 @@ const apps = relay.getAvailableApps()
 if (apps.length > 0) {
   console.log(`\n${apps.length} app row(s) across connected relays:`)
   for (const app of apps) {
-    const id = app.appId || app.appKey.slice(0, 12) + '...'
-    console.log(`  - ${id} (from relay ${app.source.relayPubkey.slice(0, 12)})`)
+    const appKey = app.appKey || app.key || ''
+    const id = app.appId || (appKey ? appKey.slice(0, 12) + '...' : 'unknown')
+    const relayPubkey = app.source && app.source.relayPubkey ? app.source.relayPubkey.slice(0, 12) : 'unknown'
+    console.log(`  - ${id} (from relay ${relayPubkey})`)
   }
 }
 
diff --git a/fleet/fleet-status.sh b/fleet/fleet-status.sh
index 1502bddc..f091849d 100755
--- a/fleet/fleet-status.sh
+++ b/fleet/fleet-status.sh
@@ -115,13 +115,21 @@ while IFS='|' read -r name host keyspec channel; do
   out="$(printf '%s\n' "$out" | tail -n 1)"
   IFS='|' read -r ver rest disk <<<"$out"
   read -r run apps conns <<<"${rest:-? ? ?}"
+  # Flag disk pressure so a filling box is caught BEFORE it wedges (a full disk
+  # hangs the relay → public 502). ! at >=75%, !! at >=90%.
+  disk_pct="$(printf '%s' "$disk" | tr -dc '0-9')"
+  disk_display="${disk:-?}"
+  if [ -n "$disk_pct" ]; then
+    if [ "$disk_pct" -ge 90 ] 2>/dev/null; then disk_display="${disk}!!"
+    elif [ "$disk_pct" -ge 75 ] 2>/dev/null; then disk_display="${disk}!"; fi
+  fi
   printf '%-9s %-9s %-8s %-6s %-6s %-6s %s\n' \
     "$safe_name" \
     "$(clean_field "${ver:-?}")" \
     "$(clean_field "${run:-?}")" \
     "$(clean_field "${apps:-?}")" \
     "$(clean_field "${conns:-?}")" \
-    "$(clean_field "${disk:-?}")" \
+    "$(clean_field "${disk_display}")" \
     "$safe_target"
 done < <(python3 -c '
 import json,sys
@@ -130,3 +138,5 @@ for r in json.load(open(sys.argv[1]))["relays"]:
     key  = "" if r.get("sshKey") in (None,"default") else r["sshKey"]
     print("{}|{}|{}|{}".format(r["name"], host, key, r.get("channel", "stable")))
 ' "$RELAYS")
+
+printf '\nDISK flags: %s = >=75%% (set/verify maxStorageBytes), %s = >=90%% (act now — a full disk wedges the relay).\n' '!' '!!'
diff --git a/package-lock.json b/package-lock.json
index 1c96e675..44d963ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5879,6 +5879,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/pear-gracedown": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/pear-gracedown/-/pear-gracedown-1.0.3.tgz",
+      "integrity": "sha512-PVh4AmpMDFxd+kMiCkuTyJ7NzRPgb0AEE7TR5GCl91x90uzWmFWon4TZMrCVhGtADG26OrQpOI+kL6S/o2w00w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bare-os": "^3.6.1",
+        "bare-signals": "^4.0.3",
+        "safety-catch": "^1.0.2",
+        "url-file-url": "^1.0.5"
+      }
+    },
     "node_modules/picomatch": {
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
@@ -7531,6 +7543,16 @@
         "bare-path": "^3.0.0"
       }
     },
+    "node_modules/url-file-url": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/url-file-url/-/url-file-url-1.0.5.tgz",
+      "integrity": "sha512-RkUWAFWSmCDpyFn2qf2QO8Gnwf0y5RadgJuDpBeFD6gEzDuh6hTNpd/jox4znFFqZ79ghAS0gGM7F3ZU4nrMWw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bare-path": "^3.0.0",
+        "which-runtime": "^1.2.0"
+      }
+    },
     "node_modules/v8-to-istanbul": {
       "version": "9.3.0",
       "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@@ -7844,7 +7866,6 @@
         "compact-encoding": "^2.15.0",
         "corestore": "^6.18.0",
         "distributed-drive": "^0.1.1",
-        "graceful-goodbye": "^1.3.0",
         "holesail": "^2.4.1",
         "hyperbee": "^2.20.5",
         "hypercore": "^10.36.0",
@@ -7854,6 +7875,7 @@
         "minimist": "^1.2.8",
         "multicast-dns": "^7.2.5",
         "paparam": "^1.10.1",
+        "pear-gracedown": "^1.0.3",
         "pino": "^9.0.0",
         "protomux": "^3.6.0",
         "socks": "^2.8.7",
diff --git a/packages/core/cli/index.js b/packages/core/cli/index.js
index 8719c7a2..edeb92f8 100755
--- a/packages/core/cli/index.js
+++ b/packages/core/cli/index.js
@@ -15,11 +15,11 @@
  */
 
 import minimist from 'minimist'
-import goodbye from 'graceful-goodbye'
+import gracedown from 'pear-gracedown'
 import { RelayNode } from '../core/relay-node/index.js'
 import { createLogger } from '../core/logger.js'
 import { isValidHexKey } from '../core/constants.js'
-import { loadConfig, saveConfig, ensureDirs, CONFIG_PATH, deriveTokenFromSeed, applyOutboxlogNamespaceEnv } from '../config/loader.js'
+import { loadConfig, saveConfig, ensureDirs, CONFIG_PATH, deriveTokenFromSeed, applyOutboxlogNamespaceEnv, resolveStorageCap } from '../config/loader.js'
 import b4a from 'b4a'
 import { existsSync, mkdirSync, cpSync, readFileSync } from 'fs'
 import { join, dirname } from 'path'
@@ -149,12 +149,20 @@ const COMMANDS = {
   help
 }
 
+// These commands parse their own positional args and already print their own
+// usage on --help / invalid input. Everything else falls back to global help.
+const SUBCOMMAND_HELP_COMMANDS = new Set(['catalog', 'federation', 'qvac', 'ghostdrive'])
+
 async function main () {
   const handler = COMMANDS[command]
   if (!handler) {
     help()
     process.exit(command ? 1 : 0)
   }
+  if ((args.help || args.h === true) && !SUBCOMMAND_HELP_COMMANDS.has(command)) {
+    help()
+    process.exit(0)
+  }
   await handler()
 }
 
@@ -453,6 +461,15 @@ async function start () {
 
   const config = loadConfig(cliOverrides)
 
+  // Scale an unset storage cap to THIS box's disk (and clamp an over-large
+  // explicit cap) so the seeder's adoption gate stops before the volume fills
+  // and wedges the node. The fixed 50 GB default is too big for small boxes.
+  resolveStorageCap(config)
+  if (config._maxStorageBytesClampedFrom != null) {
+    console.log(`  ${ARROW} ${paint(C.yellow, 'max-storage clamped')} ${formatBytes(config._maxStorageBytesClampedFrom)} ${ARROW} ${formatBytes(config.maxStorageBytes)} (90% of disk — protects the volume)`)
+    delete config._maxStorageBytesClampedFrom
+  }
+
   console.log(mainBanner(VERSION))
   console.log('  ' + ARROW + ' ' + paint(C.cyan, 'starting relay node') + ' ' + paint(C.dim, '...'))
   console.log()
@@ -591,7 +608,7 @@ async function start () {
 
   let statusInterval = null
 
-  goodbye(async () => {
+  gracedown(async () => {
     if (statusInterval) clearInterval(statusInterval)
     log.info('shutting down')
     console.log(shutdownBanner())
@@ -818,7 +835,7 @@ async function startTestnet () {
     process.stdout.write(`\r  [testnet] ${parts.join(' | ')}${clientPart}   `)
   }, 5000)
 
-  goodbye(async () => {
+  gracedown(async () => {
     clearInterval(statusInterval)
     console.log(shutdownBanner())
     console.log('  ' + ARROW + ' ' + paint(C.dim, 'tearing down testnet...'))
diff --git a/packages/core/config/default.js b/packages/core/config/default.js
index f1ec921b..77d974d5 100644
--- a/packages/core/config/default.js
+++ b/packages/core/config/default.js
@@ -151,7 +151,11 @@ export default {
     diskPressurePct: 80,
     resumePct: 70,
     floorMargin: 1,
-    minAgeMs: 3 * 24 * 60 * 60 * 1000,
+    // 6h (was 3 days). 3 days was too protective: a box ingesting content
+    // faster than that could never shed old-enough over-replicated entries and
+    // would fill the disk to 100% before disk-pressure eviction could act. The
+    // floor + floorMargin still protect availability regardless of age.
+    minAgeMs: 6 * 60 * 60 * 1000,
     sweepIntervalMs: 10 * 60 * 1000,
     maxEvictionsPerSweep: 20
   },
diff --git a/packages/core/config/loader.js b/packages/core/config/loader.js
index d0c55add..d7546751 100644
--- a/packages/core/config/loader.js
+++ b/packages/core/config/loader.js
@@ -7,7 +7,7 @@
  *   3. config/default.js (built-in defaults)
  */
 
-import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
+import { readFileSync, writeFileSync, mkdirSync, existsSync, statfsSync } from 'fs'
 import { join } from 'path'
 import { homedir } from 'os'
 import { createHmac } from 'crypto'
@@ -69,6 +69,66 @@ export function applyOutboxlogNamespaceEnv (cliOverrides = {}, rawEnvValue, hasP
   return cliOverrides
 }
 
+// Storage-cap safety fractions of the volume. The built-in default
+// (config/default.js maxStorageBytes = 50 GB) is a FIXED value that does not
+// scale to the disk — on a small box it is too big: a 58 GB box with a 50 GB
+// seeded cap fills the disk to 100% (seeded bytes + gateway store + OS + Hypercore
+// overhead) BEFORE the 50 GB cap binds, wedging the node (writes fail → /health
+// hangs → public 502). See the 2026-07 relay-us disk-full incident. So we make an
+// UNSET cap scale to the actual disk, and clamp any explicit cap that would risk
+// filling the volume.
+const DEFAULT_STORAGE_DISK_FRACTION = 0.75 // unset cap → 75% of the volume
+const MAX_STORAGE_DISK_FRACTION = 0.90 // hard ceiling any explicit cap is clamped to
+
+function existingAncestor (p) {
+  let cur = p
+  for (let i = 0; i < 64 && cur; i++) {
+    if (existsSync(cur)) return cur
+    const parent = join(cur, '..')
+    if (parent === cur) break
+    cur = parent
+  }
+  return cur
+}
+
+/**
+ * Resolve a disk-relative storage cap so an unset maxStorageBytes scales to the
+ * actual volume instead of the fixed 50 GB default. Rules:
+ *   - unset (equals the built-in default): cap = 75% of total disk.
+ *   - explicit but > 90% of disk: clamped to 90% (config._maxStorageBytesClampedFrom
+ *     records the original so the CLI can warn).
+ *   - disk unmeasurable / non-positive: config is left unchanged (safe fallback to
+ *     the merged value — never make the cap worse than it already was).
+ *
+ * Pure and testable: pass `opts.statfs` (a statfsSync-shaped fn returning
+ * { blocks, bsize }) to simulate a volume; defaults to fs.statfsSync of the
+ * storage path's nearest existing ancestor. Mutates and returns `config`.
+ */
+export function resolveStorageCap (config, opts = {}) {
+  const statfs = opts.statfs || statfsSync
+  const target = opts.storagePath || (config && config.storage) || STORAGE_DIR
+  let totalBytes = 0
+  try {
+    const s = statfs(existingAncestor(target))
+    totalBytes = Number(s.blocks) * Number(s.bsize)
+  } catch {
+    totalBytes = 0
+  }
+  if (!Number.isFinite(totalBytes) || totalBytes <= 0) return config
+
+  const diskDefault = Math.floor(totalBytes * DEFAULT_STORAGE_DISK_FRACTION)
+  const diskCeiling = Math.floor(totalBytes * MAX_STORAGE_DISK_FRACTION)
+  const isDefault = config.maxStorageBytes === defaults.maxStorageBytes
+
+  if (isDefault) {
+    config.maxStorageBytes = diskDefault
+  } else if (typeof config.maxStorageBytes === 'number' && config.maxStorageBytes > diskCeiling) {
+    config._maxStorageBytesClampedFrom = config.maxStorageBytes
+    config.maxStorageBytes = diskCeiling
+  }
+  return config
+}
+
 /**
  * Load config: defaults < config.json < CLI overrides
  */
diff --git a/packages/core/core/relay-node/api-metrics.js b/packages/core/core/relay-node/api-metrics.js
index 74e7b567..800ca677 100644
--- a/packages/core/core/relay-node/api-metrics.js
+++ b/packages/core/core/relay-node/api-metrics.js
@@ -1,6 +1,6 @@
 export function resolveMetricsRoute (method, path) {
   if (method !== 'GET') return null
-  if (path === '/metrics') return { kind: 'metrics' }
+  if (path === '/metrics' || path === '/api/metrics') return { kind: 'metrics' }
   return null
 }
 
diff --git a/packages/core/core/relay-node/api-status-read.js b/packages/core/core/relay-node/api-status-read.js
index 5b44fe91..d2fef54a 100644
--- a/packages/core/core/relay-node/api-status-read.js
+++ b/packages/core/core/relay-node/api-status-read.js
@@ -13,7 +13,7 @@ const DISK_STATUS = new Set(['ok', 'warn', 'critical'])
 
 export function resolveStatusRoute (method, path) {
   if (method !== 'GET') return null
-  if (path === '/status') return { kind: 'status' }
+  if (path === '/status' || path === '/api/status') return { kind: 'status' }
   return null
 }
 
diff --git a/packages/core/core/relay-node/api.js b/packages/core/core/relay-node/api.js
index f1a8ca4e..15bc33bc 100644
--- a/packages/core/core/relay-node/api.js
+++ b/packages/core/core/relay-node/api.js
@@ -103,6 +103,13 @@ import {
   buildStatusRoutePayload,
   resolveStatusRoute
 } from './api-status-read.js'
+import {
+  buildCreditsCompareRoutePayload,
+  buildCreditsPricingRoutePayload,
+  buildCreditsStatsRoutePayload,
+  buildCreditsWalletsRoutePayload,
+  resolveCreditsRoute
+} from './api-credits.js'
 import {
   buildUsageTelemetryRoutePayload,
   resolveUsageTelemetryRoute,
@@ -975,6 +982,7 @@ export class RelayAPI extends EventEmitter {
         const healthRoute = resolveHealthRoute(req.method, path)
         const statusRoute = resolveStatusRoute(req.method, path)
         const metricsRoute = resolveMetricsRoute(req.method, path)
+        const creditsRoute = resolveCreditsRoute(req.method, path)
         const wizardSnapshotRoute = resolveWizardSnapshotRoute(req.method, path)
 
         if (healthRoute && healthRoute.kind === 'health') {
@@ -1008,6 +1016,25 @@ export class RelayAPI extends EventEmitter {
           return writeText(res, result.text, result.status || 200)
         }
 
+        if (creditsRoute) {
+          if (creditsRoute.kind === 'credits-pricing') {
+            const result = buildCreditsPricingRoutePayload({ route: creditsRoute, node: this.node })
+            return this._json(res, result.payload, result.status || 200)
+          }
+          if (creditsRoute.kind === 'credits-pricing-compare') {
+            const result = buildCreditsCompareRoutePayload({ route: creditsRoute, node: this.node })
+            return this._json(res, result.payload, result.status || 200)
+          }
+          if (creditsRoute.kind === 'credits-stats') {
+            const result = buildCreditsStatsRoutePayload({ route: creditsRoute, node: this.node })
+            return this._json(res, result.payload, result.status || 200)
+          }
+          if (creditsRoute.kind === 'credits-wallets') {
+            const result = buildCreditsWalletsRoutePayload({ route: creditsRoute, node: this.node })
+            return this._json(res, result.payload, result.status || 200)
+          }
+        }
+
         if (peerStateRoute && peerStateRoute.kind === 'legacy-peer-list') {
           const result = buildPeerStateRoutePayload({
             route: peerStateRoute,
diff --git a/packages/core/core/relay-node/eviction.js b/packages/core/core/relay-node/eviction.js
index 236826f7..43529967 100644
--- a/packages/core/core/relay-node/eviction.js
+++ b/packages/core/core/relay-node/eviction.js
@@ -44,7 +44,7 @@ const DEFAULTS = {
   diskPressurePct: 80,
   resumePct: 70,
   floorMargin: 1,
-  minAgeMs: 3 * 24 * 60 * 60 * 1000, // 3 days
+  minAgeMs: 6 * 60 * 60 * 1000, // 6h (see config/default.js — 3 days let boxes fill before shedding)
   sweepIntervalMs: 10 * 60 * 1000,
   maxEvictionsPerSweep: 20,
   // Replica target when an entry has no replication-health row (boot-
diff --git a/packages/core/package.json b/packages/core/package.json
index 52e2ef12..00f1e49d 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -79,7 +79,6 @@
     "compact-encoding": "^2.15.0",
     "corestore": "^6.18.0",
     "distributed-drive": "^0.1.1",
-    "graceful-goodbye": "^1.3.0",
     "holesail": "^2.4.1",
     "hyperbee": "^2.20.5",
     "hypercore": "^10.36.0",
@@ -89,6 +88,7 @@
     "minimist": "^1.2.8",
     "multicast-dns": "^7.2.5",
     "paparam": "^1.10.1",
+    "pear-gracedown": "^1.0.3",
     "pino": "^9.0.0",
     "protomux": "^3.6.0",
     "socks": "^2.8.7",
diff --git a/packages/services/README.md b/packages/services/README.md
index d554a5ca..7c6bf996 100644
--- a/packages/services/README.md
+++ b/packages/services/README.md
@@ -12,13 +12,18 @@ npm install p2p-hiveservices
 ```
 
 ```js
-import { createBuiltInServices } from 'p2p-hiveservices'
+import { IdentityService, StorageProofService } from 'p2p-hiveservices'
 
-const services = createBuiltInServices({
-  enabled: ['identity', 'storage-proof']
-})
+const services = [
+  new IdentityService(),
+  new StorageProofService()
+]
 ```
 
+Services are enabled by relay operators through Core's `PluginLoader` and the
+`config.plugins` array. The package exports service classes and helpers; it does
+not provide a `createBuiltInServices` factory.
+
 The package is ESM-only and requires Node 20 or newer.
 
 ## Built-In Providers
diff --git a/standalone/client.js b/standalone/client.js
index f91bb366..0cd930fd 100644
--- a/standalone/client.js
+++ b/standalone/client.js
@@ -18,7 +18,7 @@ import Hyperswarm from 'hyperswarm'
 import ProtomuxRPC from 'protomux-rpc'
 import c from 'compact-encoding'
 import b4a from 'b4a'
-import goodbye from 'graceful-goodbye'
+import gracedown from 'pear-gracedown'
 import { createInterface } from 'readline'
 import { mkdirSync } from 'fs'
 import crypto from 'crypto'
@@ -72,7 +72,7 @@ async function main () {
   console.log('  Searching for server on DHT...\n')
 
   // 3. Graceful shutdown
-  goodbye(async () => {
+  gracedown(async () => {
     console.log('\n  Shutting down...')
     if (rpc) rpc.destroy()
     await swarm.destroy()
diff --git a/standalone/package-lock.json b/standalone/package-lock.json
index 3b57ac0f..35470b13 100644
--- a/standalone/package-lock.json
+++ b/standalone/package-lock.json
@@ -11,9 +11,9 @@
         "b4a": "^1.6.6",
         "compact-encoding": "^2.15.0",
         "corestore": "^6.18.0",
-        "graceful-goodbye": "^1.3.0",
         "hypercore": "^10.36.0",
         "hyperswarm": "^4.7.0",
+        "pear-gracedown": "^1.0.3",
         "protomux-rpc": "^1.3.0"
       },
       "devDependencies": {
@@ -107,12 +107,6 @@
         }
       }
     },
-    "node_modules/bare-abort": {
-      "version": "2.0.13",
-      "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
-      "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
-      "license": "Apache-2.0"
-    },
     "node_modules/bare-addon-resolve": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
@@ -157,15 +151,6 @@
         "bare-inspect": "^3.1.2"
       }
     },
-    "node_modules/bare-env": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
-      "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-os": "^3.0.1"
-      }
-    },
     "node_modules/bare-events": {
       "version": "2.8.2",
       "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
@@ -204,12 +189,6 @@
         }
       }
     },
-    "node_modules/bare-hrtime": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
-      "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
-      "license": "Apache-2.0"
-    },
     "node_modules/bare-inspect": {
       "version": "3.1.4",
       "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
@@ -258,34 +237,6 @@
         "bare-os": "^3.0.1"
       }
     },
-    "node_modules/bare-pipe": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
-      "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-events": "^2.0.0",
-        "bare-stream": "^2.0.0"
-      },
-      "engines": {
-        "bare": ">=1.16.0"
-      }
-    },
-    "node_modules/bare-process": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz",
-      "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-abort": "^2.0.13",
-        "bare-env": "^3.0.0",
-        "bare-events": "^2.3.1",
-        "bare-hrtime": "^2.0.0",
-        "bare-os": "^3.7.1",
-        "bare-signals": "^4.0.0",
-        "bare-stdio": "^1.0.1"
-      }
-    },
     "node_modules/bare-semver": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.3.tgz",
@@ -305,17 +256,6 @@
         "bare": ">=1.7.0"
       }
     },
-    "node_modules/bare-stdio": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz",
-      "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-fs": "^4.5.2",
-        "bare-pipe": "^4.1.5",
-        "bare-tty": "^5.0.3"
-      }
-    },
     "node_modules/bare-stream": {
       "version": "2.13.0",
       "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz",
@@ -342,20 +282,6 @@
         }
       }
     },
-    "node_modules/bare-tty": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz",
-      "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "bare-events": "^2.2.0",
-        "bare-signals": "^4.0.0",
-        "bare-stream": "^2.0.0"
-      },
-      "engines": {
-        "bare": ">=1.16.0"
-      }
-    },
     "node_modules/bare-type": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
@@ -592,16 +518,6 @@
         "which-runtime": "^1.2.0"
       }
     },
-    "node_modules/graceful-goodbye": {
-      "version": "1.3.3",
-      "resolved": "https://registry.npmjs.org/graceful-goodbye/-/graceful-goodbye-1.3.3.tgz",
-      "integrity": "sha512-ABj07mmHrK3FC8fJ3PP5QCrbA31zkB4Qg34NFmYgwJnBxzfbRmtMzOg54PL55OG5sFjvmBzD0q85JAtRlA3dSA==",
-      "license": "MIT",
-      "dependencies": {
-        "bare-process": "^4.0.0",
-        "safety-catch": "^1.0.2"
-      }
-    },
     "node_modules/hypercore": {
       "version": "10.38.2",
       "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.38.2.tgz",
@@ -882,6 +798,18 @@
         }
       }
     },
+    "node_modules/pear-gracedown": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/pear-gracedown/-/pear-gracedown-1.0.3.tgz",
+      "integrity": "sha512-PVh4AmpMDFxd+kMiCkuTyJ7NzRPgb0AEE7TR5GCl91x90uzWmFWon4TZMrCVhGtADG26OrQpOI+kL6S/o2w00w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bare-os": "^3.6.1",
+        "bare-signals": "^4.0.3",
+        "safety-catch": "^1.0.2",
+        "url-file-url": "^1.0.5"
+      }
+    },
     "node_modules/protomux": {
       "version": "3.10.1",
       "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
@@ -1188,6 +1116,16 @@
         "b4a": "^1.6.6"
       }
     },
+    "node_modules/url-file-url": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/url-file-url/-/url-file-url-1.0.5.tgz",
+      "integrity": "sha512-RkUWAFWSmCDpyFn2qf2QO8Gnwf0y5RadgJuDpBeFD6gEzDuh6hTNpd/jox4znFFqZ79ghAS0gGM7F3ZU4nrMWw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bare-path": "^3.0.0",
+        "which-runtime": "^1.2.0"
+      }
+    },
     "node_modules/which-runtime": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
diff --git a/standalone/package.json b/standalone/package.json
index 71c2d52a..c83bebc4 100644
--- a/standalone/package.json
+++ b/standalone/package.json
@@ -14,9 +14,9 @@
     "b4a": "^1.6.6",
     "compact-encoding": "^2.15.0",
     "corestore": "^6.18.0",
-    "graceful-goodbye": "^1.3.0",
     "hypercore": "^10.36.0",
     "hyperswarm": "^4.7.0",
+    "pear-gracedown": "^1.0.3",
     "protomux-rpc": "^1.3.0"
   },
   "engines": {
diff --git a/standalone/server.js b/standalone/server.js
index 21dcf49a..36683004 100644
--- a/standalone/server.js
+++ b/standalone/server.js
@@ -19,7 +19,7 @@ import Hyperswarm from 'hyperswarm'
 import ProtomuxRPC from 'protomux-rpc'
 import c from 'compact-encoding'
 import b4a from 'b4a'
-import goodbye from 'graceful-goodbye'
+import gracedown from 'pear-gracedown'
 import { mkdirSync } from 'fs'
 
 // ---------------------------------------------------------------------------
@@ -71,7 +71,7 @@ async function main () {
   console.log('\n  Waiting for peers...\n')
 
   // 4. Graceful shutdown
-  goodbye(async () => {
+  gracedown(async () => {
     console.log('\n  Shutting down...')
     for (const p of peers.values()) {
       if (p.rpc) p.rpc.destroy()
diff --git a/test/unit/config-loader.test.js b/test/unit/config-loader.test.js
index 28307ff6..fd80633e 100644
--- a/test/unit/config-loader.test.js
+++ b/test/unit/config-loader.test.js
@@ -96,7 +96,12 @@ test('cli start uses HIVERELAY_MAX_STORAGE only before saved operator config exi
   }, 'Max Store:')
 
   t.ok(saved.sawNeedle)
-  t.ok(saved.stdout.includes('Max Store:  50.0 GB'))
+  // Once a saved config.json exists, HIVERELAY_MAX_STORAGE is ignored (env is a
+  // default, not an override) and the cap falls back to the unset default. That
+  // default is now DISK-RELATIVE (resolveStorageCap → ~75% of the volume) rather
+  // than a fixed 50 GB, so assert the env value was not applied instead of a
+  // fixed number that depends on the test machine's disk.
+  t.absent(saved.stdout.includes('Max Store:  10.0 GB'), 'HIVERELAY_MAX_STORAGE not applied once a saved config exists')
 })
 
 test('cli start uses HIVERELAY_STORAGE when --storage is absent', async (t) => {