From e5b88781d97f4dbe73313d925ee4ba07796814e5 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:33:59 +0000 Subject: [PATCH 01/12] Worker dashboard: scheduling classes on the cards (kind, nice, cores), gate-queued marker, wait and gate-pending files The slot line now ends "; kind= nice= cores=; agent=" (build box scheduling, 69034dc0): lib.mjs lifts the three into their own fields and keeps the bare label; the old line shape still derives its kind. The collector reads /srv/builds/_locks/wait- and gate-pending- directly (dead pids skipped), marks a gate with priority gate and sorts it first; the page shows the class line on Now building and Queue cards, a "gate queued" pill, and the class in the closing lines. JSONL nice, cores, jobs and priority carried through. 14 tests pass. Co-Authored-By: Claude Fable 5.1 --- tools/workers/collect.mjs | 23 +++++++++++++++++------ tools/workers/lib.mjs | 23 +++++++++++++++++++---- tools/workers/page/workers.html | 8 ++++---- tools/workers/shape.js | 5 ++++- tools/workers/test/parsers.test.mjs | 18 ++++++++++++++++++ 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/tools/workers/collect.mjs b/tools/workers/collect.mjs index 42131a8c..fad99beb 100644 --- a/tools/workers/collect.mjs +++ b/tools/workers/collect.mjs @@ -18,7 +18,7 @@ import { spawnSync } from 'node:child_process'; import { hostname } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parseProcStat, cpuBusy, parseMeminfo, parseNetDev, parseLoadavg, parseLockDir, parseBuildsJsonl, parseSccacheStats, pickTemps, cargoProcess, deriveKind, BASELINES } from './lib.mjs'; +import { parseProcStat, cpuBusy, parseMeminfo, parseNetDev, parseLoadavg, parseLockDir, parseBuildsJsonl, parseSccacheStats, pickTemps, cargoProcess, deriveKind, parseSlotLine, BASELINES } from './lib.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const argv = process.argv.slice(2); @@ -68,6 +68,7 @@ function slots(now) { } function procs(bootSec, clk, now) { const running = [], waiters = []; let compilers = 0; + const flockParents = new Map(); // ppid -> since, from the flock -w processes the waiters run for (const d of safeList('/proc')) { if (!/^\d+$/.test(d)) continue; const pid = Number(d); const comm = read(`/proc/${d}/comm`).trim(); @@ -82,17 +83,26 @@ function procs(bootSec, clk, now) { if (args[1] === '-w' && /^\d+$/.test(args[2] || '')) { const stat = read(`/proc/${d}/stat`); const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' '); const ppid = Number(rest[1]); const startTicks = Number(rest[19]); - const since = Number.isFinite(startTicks) ? iso((bootSec + startTicks / clk) * 1000) : null; - const wait = read(join(LOCKS, `wait-${ppid}`)).trim(); // a waiter's own line, when the appender writes one - waiters.push({ pid: ppid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: wait || null, ...(wait ? { kind: deriveKind(parseLabelSafe(wait)) } : {}) }); + flockParents.set(ppid, Number.isFinite(startTicks) ? iso((bootSec + startTicks / clk) * 1000) : null); } } } - // the innermost cargo per worktree: a `cargo build` spawns no nested cargo, but a `cargo test` runs test binaries, not cargo + // the waiters' own lines: wait- (a queued build) and gate-pending- (a gate that takes the next free slot + // ahead of queued suites and benches), written by remote-run.sh in the slot-file format and removed on take or give-up; + // a file whose pid is gone is a leftover, not a waiter + const seen = new Set(); + for (const f of safeList(LOCKS)) { + const m = /^(wait|gate-pending)-(\d+)$/.exec(f); if (!m) continue; + const pid = Number(m[2]); if (!alive(pid) || seen.has(pid)) continue; seen.add(pid); + const line = parseSlotLine(read(join(LOCKS, f)).split('\n')[0], { now }); + const since = (line && line.since) || flockParents.get(pid) || null; + waiters.push({ pid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: line ? line.label : null, kind: line ? deriveKind(line) : null, agent: line ? line.agent : null, worktree: line ? line.worktree : null, crate: line ? line.crate : null, command: line ? line.command : null, nice: line ? line.nice : null, cores: line ? line.cores : null, priority: m[1] === 'gate-pending' ? 'gate' : 'normal' }); + } + for (const [ppid, since] of flockParents) if (!seen.has(ppid)) waiters.push({ pid: ppid, since, waiting_s: since ? Math.max(0, Math.round((now - Date.parse(since)) / 1000)) : null, label: null, priority: 'normal' }); + waiters.sort((a, b) => (a.priority === 'gate' ? 0 : 1) - (b.priority === 'gate' ? 0 : 1) || String(a.since || '').localeCompare(String(b.since || ''))); running.sort((a, b) => String(a.started_at).localeCompare(String(b.started_at))); return { running, waiters, compilers }; } -function parseLabelSafe(line) { const r = /: (.*)$/.exec(line); return { command: r ? r[1] : line }; } function sccache() { const bin = existsSync('/home/build/.cargo/bin/sccache') ? '/home/build/.cargo/bin/sccache' : 'sccache'; @@ -121,6 +131,7 @@ export function collect() { for (const r of p.running) { const h = s.held.find(h => h.worktree === r.worktree && (!h.crate || h.crate === r.crate || !r.crate)); r.slot = h ? h.slot : null; r.agent = h ? h.agent : null; r.queued_at = h ? h.since : null; r.waited_s = h ? h.waited_s : null; + if (h && h.class) r.kind = h.class; r.nice = h ? h.nice : null; r.cores = h ? h.cores : null; } const logText = read(LOG); const recent = parseBuildsJsonl(logText, { limit: 200 }); diff --git a/tools/workers/lib.mjs b/tools/workers/lib.mjs index 71f4ad55..6933e914 100644 --- a/tools/workers/lib.mjs +++ b/tools/workers/lib.mjs @@ -65,15 +65,28 @@ export function parseSlotLine(line, { day, now } = {}) { const d = day || new Date(nowMs).toISOString().slice(0, 10); let since = Date.parse(`${d}T${r[2]}:${r[3]}:${r[4]}Z`); if (since > nowMs + 60_000) since -= 86_400_000; - const label = r[6].trim(); - const agent = (/;\s*agent=([^;]+)\s*$/.exec(label) || [])[1] || null; - return { pid: Number(r[1]), since: new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z'), waited_s: Number(r[5]), label: agent ? label.replace(/;\s*agent=[^;]+\s*$/, '').trim() : label, agent, ...splitLabel(label) }; + const parts = splitLabelParts(r[6]); const sl = splitLabel(parts.label); + return { pid: Number(r[1]), since: new Date(since).toISOString().replace(/\.\d{3}Z$/, 'Z'), waited_s: Number(r[5]), label: parts.label, agent: parts.agent, class: parts.class, nice: parts.nice, cores: parts.cores, ...sl, kind: parts.class || deriveKind({ command: sl.command, crate: sl.crate, target: sl.target }) }; +} + +/// The label's trailing segments (7 October 2026, build box scheduling classes): "

Every entry is a dated heading of the engineering log, where the commands and the hardware are. The phases and their gates are the litepaper’s roadmap.

diff --git a/site/journey.json b/site/journey.json index f0402beb..9f39477e 100644 --- a/site/journey.json +++ b/site/journey.json @@ -28,10 +28,10 @@ { "id": "phase-4", "name": "Finality and job market", - "when": "Closes when the finality design passes external review and one rollup signs for the testnet", + "when": "Closes when the finality design passes external review", "status": "next", "line": "Sustained-mining finality, external proving jobs, miner client with auto-switching", - "gate": "Finality design passes external review and one rollup signs for testnet" + "gate": "Finality design passes external review" }, { "id": "phase-5", @@ -44,7 +44,7 @@ { "id": "phase-6", "name": "Mainnet fair launch", - "when": "After the testnet has passed its gate: 1,000 independent miners for 30 days and rollup proofs on time", + "when": "After the testnet has passed its gate (1,000 independent miners for 30 days, rollup proofs on time) and one proving customer has signed: a customer paying for proofs at a published rate, or a letter of intent with a volume", "status": "next", "line": "Genesis with no premine, 30-day ramp. No listing is arranged, promised or sought by the project" } @@ -207,48 +207,48 @@ }, { "date": "2026-10-04", - "text": "Sim/economy: mining versus proving under stress, agent-based", - "short": "Economy simulation: mining versus proving under stress" + "text": "One-click Windows workers: what the Apple M5 Max could measure", + "short": "One-click Windows workers" }, { "date": "2026-10-04", - "text": "Difficulty rule under attack: pool hopping, pulsed rental, timestamp stretching, short-lane oscillation, epoch games, polluted window, block flood", - "short": "Difficulty rule attacked seven ways" + "text": "First finality lock on the live devnet: checkpoint 242 at 77.4% of all weight, two hours after genesis", + "short": "First live finality lock: 77.4% of weight, 17 voters" }, { "date": "2026-10-04", - "text": "Difficulty rule: timestamp attack fixed , simulator regression, 3-node forger test", - "short": "Timestamp attack on the difficulty rule fixed" + "text": "The gfx1036 worker fault and what the Apple M5 Max could and could not reproduce", + "short": "The gfx1036 worker fault and what the Apple M5 Max could and could…" }, { "date": "2026-10-04", - "text": "Devnet-v4 integration: nine branches merged, 3-node test network on the merged node, Windows cross-build", - "short": "Devnet v4: nine branches merged into one node" + "text": "A node 60 s behind the clock is silently dead", + "short": "A node 60 s behind the clock is silently dead" }, { "date": "2026-10-04", - "text": "Generator version 2 adopted: exact load count, fresh-source loads, program acceptance; every vector re-cut, three workers re-checked, 20,000-program census, devnet-v4 binaries rebuilt", - "short": "Generator v2 adopted: every hash does 128 distinct reads" + "text": "First machine on the Igneum Miner app: the RTX 5090 Windows rig's RTX 5090 at 118 MH/s, via Setup.exe", + "short": "First machine on the one-click app: a 5090 at 118 MH/s" }, { "date": "2026-10-04", - "text": "Proving v0 on the RTX 5090: first GPU proof of an Igneum block", - "short": "First GPU proof of an Igneum block: 1.4 s on an RTX 5090" + "text": "Difficulty rule v2: the live oscillation, its cause, the DAG replay, the fix behind a height switch", + "short": "Difficulty rule v2" }, { "date": "2026-10-04", - "text": "Devnet v4 cut-over: generator v2, 2/3 floor, three nodes and a seed on a fresh chain", - "short": "Devnet v4 live: generator v2, two-thirds floor, fresh chain" + "text": "The observer stored nothing for 78 minutes, then 7,022 blocks in two minutes", + "short": "The observer stored nothing for 78 minutes, then 7,022 blocks in two…" }, { "date": "2026-10-04", - "text": "First hourly program swap on the live devnet: compile-ahead, no pause, two cards", - "short": "First live hourly swap: no pause on Mac, NVIDIA or AMD" + "text": "The RTX 5090 Windows rig at the 14:20 boundary: a worker stuck on the previous epoch", + "short": "The RTX 5090 Windows rig at the 14" }, { "date": "2026-10-04", - "text": "Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine", - "short": "Proving: devnet v4 shards on the Apple M5 Max CPU, loaded machine" + "text": "Shard proving on the RTX 5090: a full shard compressed in 10.9 s, a two-shard block aggregated in 2.2 s, all verified", + "short": "Shard layer on the RTX 5090: a full shard proven in 10.9 s, a block aggregated in 2.2 s" } ] } diff --git a/site/litepaper.html b/site/litepaper.html index a8b09baa..dbc73d68 100644 --- a/site/litepaper.html +++ b/site/litepaper.html @@ -490,6 +490,18 @@ body.all .pager{display:none}

No fund, no foundation, no fee to the team

There is no development fund. A switch that routes money to an address somebody controls is the first thing a critic points at, so Igneum has none. The protocol carries no fee to any team, foundation or fund. The project earns in the open, and this is the full list: the optional 1% dev fee in the Ember miner software, off with one flag and audited on the chain; the provers it runs in the job market; any pool it operates; and the app share on the contracts it deploys, like anyone else. None of it is in the protocol, and the first item is the one a critic should quote: for as long as miners run Ember with the fee on, 1 block in 100 pays the project. If the community ever wants a grant mechanism, miners can add one by signalling.

+

What the chain is, in law

+

Not legal advice; counsel is engaged. These are the facts of the design that the rules of each region turn on; nothing here is a promotion of anything.

+
    +
  1. No issuer, no offeror, no sale. Every coin is created automatically as a reward for the maintenance of the distributed ledger or the validation of transactions, and in no other way.
  2. +
  3. The sustainability indicators are published. The indicators of Delegated Regulation 2025/422 (energy in kWh a year from the network’s hash rate and the measured microjoules per hash, intensity per transaction, the regional mix when it is known) are published by the project every era, so a service provider in the EU can list the coin without asking.
  4. +
  5. No financial promotion. No price, no “buy”, no “invest”, no return language anywhere. The earnings screen shows hash and IGN, never a currency.
  6. +
  7. The reference pool never holds a member’s balance. Payouts come straight from the coinbase split; the pool coordinates, it does not keep custody.
  8. +
  9. The job market settles peer to peer on the chain. There is no operator account and no dollar leg in the protocol: the dollar figure is a quote, the settlement is IGN.
  10. +
  11. Open software, no coins by right. The software is published under an open licence by a company that holds no coins by right and runs no service the chain’s consensus depends on. There is no dev fund.
  12. +
  13. Mining may be restricted where you are; you are responsible for checking. The miner asks your region at first run and refuses the regions where mining is banned (on 7 October 2026: ten regions of Russia and Moscow from 15 August 2026, and China).
  14. +
  15. No privileged key. No key can mint, pause or upgrade the chain; the only way a rule changes is miners signalling for it, and nothing requires them to.
  16. +
@@ -504,6 +516,7 @@ body.all .pager{display:none} External proving jobsRollups and apps on other chains, priced in their moneyNo, but the market is small today and is upside, not a promise +

A block pays its miner whether or not anyone buys a proof that day. The lottery pays 80 percent of every block from emission; proving is the second income, never the only one. Every useful-work chain on record dropped its miners the day the work stopped paying; Igneum’s miners are paid for the block first.

The size of that third stream today, in numbers: all of Ethereum L1's proving is about USD 36 a day at the September 2026 tracker cost (USD 0.005 a block, 7,200 blocks a day; the tracker figure is a secondary source), against about USD 13,700 a day of Igneum's year-1 emission at USD 0.005 per IGN (31.688 IGN a block, 86,400 blocks a day; the price is an input, not a forecast). So external proving is a small second income at launch and the lottery pays the bills; for proving to become the main income the paid demand would have to grow about 1,000x in dollars (the Horizon lane analysis, 6 October 2026, section 3.11; ledger E19).

The honest bear-market case rests on cost. A miner's card is already running and the power is often domestic, so Igneum miners' electricity cost in the proving market is close to power. The price they must charge is another matter: the price a prover must charge is the subsidy it forgoes while it proves, which falls as one over network hash, so the edge over data-centre provers appears only once the network's hash is large (near 100 GH/s for a card proving beside its miner) and is nothing more. Which of the two in-chain streams pays more per GPU-second depends on the size of the fleet: on the devnet of 4 October 2026, three machines at 275 million hashes a second, a second of hashing paid about 4.9x a second of proving the pool share; at 10,000 cards the same arithmetic favours proving by about 930x. That is arithmetic on measured devnet rates, approximate, not a market measurement.

Hardware

diff --git a/site/live.html b/site/live.html index 44968691..9a51d200 100644 --- a/site/live.html +++ b/site/live.html @@ -148,6 +148,9 @@ .legend-row .lg span{display:inline-flex;align-items:center;gap:7px;color:var(--ink-2);font-family:var(--f-sans);font-size:12px} .sw{width:9px;height:9px;transform:rotate(45deg);display:inline-block;flex:none} .three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4);margin-top:var(--s-4)} +.two{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:var(--s-4);margin-top:var(--s-4)} +.lb table td,.lb table th{white-space:nowrap}.lb .you td{color:var(--molten-text)} +@media (max-width:900px){.two{grid-template-columns:1fr}} @media (max-width:1000px){.three{grid-template-columns:1fr}} .dp{border:var(--hair);border-radius:7px;background:var(--graphite);padding:20px 21px 16px;min-width:0} .dp .ph{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:14px} @@ -359,6 +362,22 @@ details.tablebar summary{display:flex;align-items:center} +
+
+

Weight leaderboard

30-day weight
+
+ + +
RankKeyBlocks in windowPresenceLast block
Waiting for the first read.
+

Weight is blue blocks over the window, never hashrate: a card that arrived today sits at the bottom. The dust line and the window are read from the node.

+
+
+

Who is mining

10 minutes
+
Waiting for the first read.
+

Counted from the observer’s reply. The card-by-card hardware census arrives with the app’s “Make my page public” switch; until then no card model is shown.

+
+
+
Explore blocks as a table keyboard-accessible view
@@ -458,6 +477,15 @@ details.tablebar summary{display:flex;align-items:center} document.addEventListener('click',function(e){var t=e.target;if(t&&t.id==='mine-btn'){var v=prompt('Your vote key’s short id, the first 8 hex characters shown in the app or the explorer:',mine||'');if(v!==null){mine=v.trim().toLowerCase();try{if(mine)localStorage.setItem('igneum-my-key',mine);else localStorage.removeItem('igneum-my-key');}catch(x){}remount();if(lastD)render(lastD);}} var row=t&&t.closest?t.closest('tr[data-hash]'):null;if(row&&dag){dag.select(row.getAttribute('data-hash'),'table');}}); function render(d){var s=d.state,pv=d.proving,fn=d.finality,now=new Date(d.now).getTime();lastD=d; + // the weight leaderboard (the finality weights the observer relays) and the census by client (the miners' engines) + (function(){var w=fn&&fn.weights,keys=w&&w.keys?w.keys.slice():[],p=fn&&fn.params||{};keys.sort(function(a,b){return (b.blocks||0)-(a.blocks||0);}); + var seen={};(d.miners||[]).forEach(function(m){seen[m.id]=m;}); + $('w-tag').textContent=keys.length?keys.length+' keys · window '+fmtN(p.weightWindow)+' DAA · dust '+fmtN(p.dust):'30-day weight'; + $('w-body').innerHTML=keys.length?keys.slice(0,24).map(function(k,i){var m=seen[k.id],last=m&&m.last_seen?Math.max(0,Math.round((now-new Date(m.last_seen).getTime())/1000))+' s ago':'not in 10 min'; + return '';}).join('')+(keys.length>24?'':''):''; + var tot=(d.miners||[]).length,voters=keys.filter(function(k){return k.voter;}).length,signing=keys.filter(function(k){return k.voter&&k.participation>=.9;}).length,lock=null;(fn&&fn.checkpoints||[]).forEach(function(c){if(c.state==='locked'&&(!lock||c.index>lock.index))lock=c;}); + var rows=[['Keys with a block in 10 min',tot,tot],['Voters above the dust line',voters,keys.length||tot],['Voters signing 90% of points or more',signing,voters],['Weight signing at the last lock',lock&&lock.fraction_total!==null&&lock.fraction_total!==undefined?Math.round(lock.fraction_total*100)+'%':'pending',null]]; + $('census').innerHTML=rows.map(function(r){var pc=r[2]&&typeof r[1]==='number'?r[1]/r[2]*100:(typeof r[1]==='string'&&/%$/.test(r[1])?parseFloat(r[1]):0);return '
'+r[0]+''+(typeof r[1]==='number'?fmtN(r[1])+(r[2]&&r[2]!==r[1]?' of '+fmtN(r[2]):''):r[1])+'
';}).join('');})(); // the six tiles set('s-bps',s.blocks_per_second_60s!==null&&s.blocks_per_second_60s!==undefined?Number(s.blocks_per_second_60s).toFixed(2):'pending'); set('s-blue',fmtN(s.blue_score));set('s-hash',fmtHash(s.hashes_per_second_estimate)); diff --git a/site/miner.html b/site/miner.html index 8bf15677..54da676c 100644 --- a/site/miner.html +++ b/site/miner.html @@ -217,6 +217,28 @@ pre b{color:var(--molten-text);font-weight:500} +
+
+
+
The ladder

Seven rungs. Every one is a fact on the chain.

+

The app lights a rung the moment the chain does. Every threshold is read from the node, never a constant: the devnet reads 5 blocks to a vote and 2 of 2 hours of presence; mainnet reads 100 blocks and 8 of 8.

+
+
'+(i+1)+''+esc(k.id)+(mine&&k.id===mine?' (you)':'')+''+fmtN(k.blocks)+''+(!k.voter?(k.stripped_until_daa?'stripped':'under dust'):k.participation===null||k.participation===undefined?'pending':Math.round(k.participation*100)+'%')+''+last+'
and '+(keys.length-24)+' more keys
No weights in the reply.
+ + + + + + + + + + +
RungWhat lights itThe line in the appDevnetMainnet
1 · First blockYour key finds a block“Block 1,284,117 is yours.”1 block1 block
2 · Your key has a voteYour blocks in the 30-day window pass the dust line“61 of 100 blocks to a vote.”5 blocks100 blocks
3 · Your signature is in a checkpointYour key signs a checkpoint that locks“Your signature is in checkpoint 184,220.”1 lock1 lock
4 · Full windowBlocks on every day of the weight window“30 of 30 days. Full weight.”2 of 2 hours30 of 30 days
5 · Rank by weightYour key’s place in the table, by blue blocks over the window“Rank 41 of 1,204 keys.”by weightby weight
6 · Signing streakEvery presence point signed, no gap over the presence window“Signing 8 of 80 points, 41 days unbroken.”2 of 28 of 8
7 · Your card proved a shardA paid shard record carries your prover key“Your card proved shard 3 of block 1,284,117.”1 shard1 shard
+

Weight, never hashrate: a card that arrived today sits at the bottom of the table and climbs one day at a time. The live table shows every key by weight.

+ +
+
From fc748466881c927be7b426887174e2e630186001 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:40:47 +0000 Subject: [PATCH 07/12] site-copy-pass: row 4a, the home triplet swap of the launch pack (the redesign lane's ask) Co-Authored-By: Claude Fable 5.1 --- docs/plans/site-copy-pass.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plans/site-copy-pass.md b/docs/plans/site-copy-pass.md index a21591d2..77aff9d4 100644 --- a/docs/plans/site-copy-pass.md +++ b/docs/plans/site-copy-pass.md @@ -15,6 +15,7 @@ tables, no orphan word on a line (tools/ci/site-orphan-check.mjs at 390 px). | 2 | Home, principles | (none; the home page was the scene and three facts) | Graphics cards only / The miners are the provers / No premine, no stake / Every claim has a status, one line each under the hero | | 3 | Home, live panel | (the scene with no heading) | Follow the work. See the chain grow. + "Every square is a real block, read from one node every 2 s. Nothing here is a replay." | | 4 | Home, reasons | (none) | Three reasons to power on: A program that never holds still / Work you can see / One click, and it mines, each in plain words with the measured limit named | +| 4a | Home, reasons (7 October 2026, the launch pack) | Three reasons to power on | Three things, in order: Your card stays a card / Income with no cliff / A fair supply, the three asks of twenty miner launch posts (docs/plans/launch-pack.md section 4 A), in the same three cards; the income card carries no emission number until the genesis re-cut, and the line under the grid points at the litepaper's finality section | | 5 | Home, economics | (on the litepaper only) | Made for the people running the hardware. Every block pays the card that found it and the cards that prove it. The protocol carves out nothing for a team, a foundation or a fund. | | 6 | Home, CTA band | (none) | Give your GPU something to do. Download Ember, watch the devnet, and tell us what breaks. The people on the Discord run cards like yours. | | 7 | Miner, hero | Install. Start. The card mines. (kept) | The miner built around your card. Your rate, your watts, what it costs a day at your price, and the chain drawn live with your own blocks ringed. | From b9e2a0dce7e694f66d82034741334f7a58adf4b4 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:41:49 +0000 Subject: [PATCH 08/12] Worker dashboard: a flock-held slot with an empty file counts as held (label unknown), not free Seen on igneum-build-1 at 10:39Z on 7 October 2026: both slot files flock-held and empty while two suites ran and three waiters queued, so the header read 0/2. The holder's missing line is the build-server agent's to fix; the collector no longer reports a full box as free either way, and the page shows "held, no holder line" in the lane. Co-Authored-By: Claude Fable 5.1 --- tools/workers/lib.mjs | 1 + tools/workers/page/workers.html | 1 + tools/workers/test/parsers.test.mjs | 3 +++ 3 files changed, 5 insertions(+) diff --git a/tools/workers/lib.mjs b/tools/workers/lib.mjs index 6933e914..52063e9f 100644 --- a/tools/workers/lib.mjs +++ b/tools/workers/lib.mjs @@ -130,6 +130,7 @@ export function parseLockDir(files, { day, now, aliveFn } = {}) { const alive = line && aliveFn ? aliveFn(line.pid) : null; const isHeld = f.held ?? (line ? alive !== false : false); if (line && isHeld) held.push({ slot: f.name, ...line, kind: deriveKind(line) }); + else if (f.held === true) held.push({ slot: f.name, pid: null, since: null, waited_s: null, label: null, agent: null, worktree: null, crate: null, command: null, target: null, kind: null, nice: null, cores: null, no_line: true }); // the flock is taken but the holder wrote no line (7 Oct 2026: the scheduling-class path on build-1) else free.push(f.name); } held.sort((a, b) => a.slot.localeCompare(b.slot)); diff --git a/tools/workers/page/workers.html b/tools/workers/page/workers.html index d668e6f5..ae026e32 100644 --- a/tools/workers/page/workers.html +++ b/tools/workers/page/workers.html @@ -244,6 +244,7 @@ function renderCrew() { function nowItems() { const out = []; for (const bx of boxesOf(D)) for (const r of bx.running || []) out.push({ where: bx.name, slot: r.slot, title: `${kindLabel(r.kind)}`, sub: `${r.worktree || '?'}/${r.crate || ''}${r.agent ? ' · ' + r.agent : ''}`, cmd: r.command, started_at: r.started_at, kind: r.kind, extra: [r.target || '', classLine(r)].filter(Boolean).join(' · ') }); + for (const bx of boxesOf(D)) for (const h of (bx.slots && bx.slots.held || []).filter(h => h.no_line && !(bx.running || []).some(r => r.slot === h.slot))) out.push({ where: bx.name, slot: h.slot, title: 'held, no holder line', sub: 'the slot is flock-held but its file is empty (the holder wrote no line)', cmd: '', started_at: null, kind: null, extra: '' }); for (const h of (D.mac && D.mac.slots.held) || []) out.push({ where: D.mac.name, slot: h.slot, title: h.slot === 'measure' ? 'measurement' : h.slot.startsWith('run') ? 'functional run' : kindLabel(h.kind), sub: h.worktree || 'this Mac', cmd: h.command || h.label, started_at: h.since, kind: h.kind, extra: h.waited_s ? `waited ${fmtDur(h.waited_s)}` : '' }); for (const pc of (D.pcs && D.pcs.machines) || []) if (pc.running) out.push({ where: pc.name, slot: 'relay job', title: `${pc.running.kind} ${pc.running.job}`, sub: pc.running.title || pc.machine || '', cmd: pc.running.stage ? `stage ${pc.running.stage}` : 'reports every 5 min while it runs', started_at: pc.running.started_at, kind: pc.running.kind, extra: pc.running.last_report_at ? `last report ${ago(pc.running.last_report_at)}` : '' }); return out; diff --git a/tools/workers/test/parsers.test.mjs b/tools/workers/test/parsers.test.mjs index c8c3dc79..af11b1fc 100644 --- a/tools/workers/test/parsers.test.mjs +++ b/tools/workers/test/parsers.test.mjs @@ -72,6 +72,9 @@ test('lock dir fixture: one held slot, one stale line, one empty, count from slo // an explicit flock verdict overrides the pid check const r2 = parseLockDir(files.map(f => ({ ...f, held: f.name === 'build-1' })), { day: '2026-10-06', now: '2026-10-06T18:00:00Z' }); assert.deepEqual(r2.held.map(h => h.slot), ['build-1']); + // a flock-held slot whose file is empty is still held (7 Oct 2026 on build-1: two held, both empty, the box full) + const r3 = parseLockDir([{ name: 'build-2', text: '', held: true }, { name: 'build-0', text: '', held: false }, { name: 'slots', text: '2' }], { now: '2026-10-07T10:39:00Z' }); + assert.equal(r3.held.length, 1); assert.equal(r3.held[0].slot, 'build-2'); assert.equal(r3.held[0].no_line, true); assert.deepEqual(r3.free, ['build-0']); }); test('builds JSONL fixture: five rows, one bad line, newest first, derived fields', () => { From 6aa5afb5104999d35b9177334267c7c1815b2e3b Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:43:01 +0000 Subject: [PATCH 09/12] Worker dashboard: the slots gauge counts build-* slots only (the box's measure lock is not a build slot) Co-Authored-By: Claude Fable 5.1 --- tools/workers/page/workers.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/workers/page/workers.html b/tools/workers/page/workers.html index ae026e32..4d103794 100644 --- a/tools/workers/page/workers.html +++ b/tools/workers/page/workers.html @@ -192,6 +192,7 @@ function arc(pct, cls) { const tone = (pct, warn = 70, bad = 90) => pct >= bad ? 'bad' : pct >= warn ? 'warn' : ''; const FEEDS = { 'igneum-build-1': 'https://build.igneum.network/workers.json', 'igneum-build-2': 'https://build-2.igneum.network/workers.json', 'igneum-build-3': 'https://build-3.igneum.network/workers.json' }; +const buildHeld = b => (b.slots && b.slots.held || []).filter(h => /^build-\d+$/.test(h.slot)).length; const boxesOf = d => (Array.isArray(d.boxes) && d.boxes.length ? d.boxes : d.box ? [{ ...d.box, source: d.sources && d.sources.box, headline: d.headline }] : []); function serverSection(b, i) { const id = `srv${i}`; @@ -211,7 +212,7 @@ function serverSection(b, i) {
${arc(m.used_pct, tone(m.used_pct))}
Memory
${m.used_pct ?? '?'}%
${esc(fmtBytes((m.used_kb || 0) * 1024))} of ${esc(fmtBytes((m.total_kb || 0) * 1024))}
${arc(d.used_pct, tone(d.used_pct, 80, 92))}
Disk ${esc(d.mount || '/srv')}
${d.used_pct ?? '?'}%
${esc(fmtBytes(d.used_bytes))} used, ${esc(fmtBytes(d.avail_bytes))} free
${arc(hit ?? 0, 'cool')}
sccache hits
${hit === null ? '–' : hit + '%'}
${sc ? `${sc.hits ?? 0} hit, ${sc.misses ?? 0} miss, ${esc(sc.cache_size || '?')} of ${esc(sc.max_size || '?')}` : 'sccache not answering'}
-
${arc(b.slots ? (b.slots.held.length / Math.max(1, b.slots.count)) * 100 : 0, 'good')}
Build slots
${b.slots ? `${b.slots.held.length}/${b.slots.count}` : '–'}
${b.queue && b.queue.length ? `${b.queue.length} waiting${b.queue.some(q => q.priority === 'gate') ? ', a gate first' : ''}` : 'nobody waiting'}
+
${arc(b.slots ? (buildHeld(b) / Math.max(1, b.slots.count)) * 100 : 0, 'good')}
Build slots
${b.slots ? `${buildHeld(b)}/${b.slots.count}` : '–'}
${b.queue && b.queue.length ? `${b.queue.length} waiting${b.queue.some(q => q.priority === 'gate') ? ', a gate first' : ''}` : 'nobody waiting'}
`; } function renderServers() { const bx = boxesOf(D); $('servers').innerHTML = bx.length ? bx.map(serverSection).join('') : serverSection(null, 0); } From 8c7d727e6189c16640d7edf1384af39053cbf492 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:57:32 +0000 Subject: [PATCH 10/12] site-redesign: the package's look 1:1, header and footer included The owner, 12:4x UK: "some things look different on the live site vs the one I sent you, match it 1:1". site/site.css is now the package's design system as delivered (tokens, type, chrome, components, breakpoints) with self-hosted fonts, the two text tokens for contrast on light surfaces, and a compatibility layer for the data pages' vocabulary. The header and footer are the package's (status bar, logo with the ember stop, the six items, the Discover dropdown, the theme toggle, the Download button, the burger and its sheet; the footer grid with the three icon links, Run, Read, Explore and the legal line). The home, miner and wallet pages carry the package's markup on our content; the live page, the explorer, the litepaper, the faucet, the MetaMask page and the generated pages take its values in their own blocks. The theme is always set (stored, else the OS), so the package's light theme applies. Side-by-side sheets before and after at 1440 px from tools/site-redesign/sheets.mjs; no sample data, no review chrome, the hero loop continuous. Co-Authored-By: Claude Fable 5.1 --- site/404.html | 130 +++++---- site/address.html | 150 +++++----- site/app.html | 140 +++++---- site/bench.html | 141 +++++---- site/block.html | 150 +++++----- site/build.mjs | 27 +- site/claims.html | 138 +++++---- site/dev-fee.html | 140 +++++---- site/evidence.html | 130 +++++---- site/explorer.html | 150 +++++----- site/faucet.html | 140 +++++---- site/index.html | 337 +++++++-------------- site/journey.html | 142 +++++---- site/ledger.html | 150 +++++----- site/litepaper.html | 173 ++++++----- site/live.html | 198 +++++++------ site/metamask.html | 134 +++++---- site/miner.html | 189 ++++++------ site/miners.html | 141 +++++---- site/partials/footer.html | 74 +++-- site/partials/head.html | 3 +- site/partials/nav.html | 53 ++-- site/provenance.html | 141 +++++---- site/randomx.html | 138 +++++---- site/scenes.html | 128 ++++---- site/site.css | 515 ++++++++------------------------- site/wallet.html | 159 +++++----- tools/ledger-page.mjs | 22 +- tools/site-redesign/sheets.mjs | 43 +++ 29 files changed, 2080 insertions(+), 2096 deletions(-) create mode 100644 tools/site-redesign/sheets.mjs diff --git a/site/404.html b/site/404.html index 89689ae5..012e1065 100644 --- a/site/404.html +++ b/site/404.html @@ -13,6 +13,7 @@ + @@ -157,71 +177,67 @@ main{padding-bottom:var(--sec)} - diff --git a/site/app.html b/site/app.html index b093527a..737960a6 100644 --- a/site/app.html +++ b/site/app.html @@ -31,6 +31,7 @@ + @@ -158,71 +178,67 @@ main{padding-bottom:var(--sec)} - diff --git a/site/build.mjs b/site/build.mjs index 7d7b36eb..76b992e9 100644 --- a/site/build.mjs +++ b/site/build.mjs @@ -112,8 +112,7 @@ ${HEAD} @@ -179,71 +199,67 @@ main{padding-bottom:var(--sec)} - diff --git a/site/faucet.html b/site/faucet.html index 4cf1f984..e4ef11f1 100644 --- a/site/faucet.html +++ b/site/faucet.html @@ -27,6 +27,7 @@ + - - +
- -
-
-
-
-
Built for GPU miners / devnet live
-

Your GPU has more to give.

-

You already own the card. Igneum gives it a job worth doing: mine the block, prove it, watch it lock. One click installs the node, the miner and the prover.

- -
Devnet build. Coins have no value. The chain may reset.
-
-
- -
IGNEUM / EMBERYour card becomes the network
-
01 / MiningIllustration
-
The card runs this hour’s program. A block comes out.Drawn, not live telemetry. The live graph is below.
-
GPU > BLOCK > PROOF > LOCK
-
- - - -
-
-
-
-
Graphics cards onlyA new mining program every hour
-
The miners are the proversSame card, two jobs, 80/20 of every block
-
No premine, no stakeNo fee to any team in the protocol
-
Every claim has a statusThe evidence page says how far each one is tested
-
-
-
- -
-
+
Built for GPU miners / devnet live

Your GPU has
more to give.

You already own the card. Igneum gives it a job worth doing: mine the block, prove it, watch it lock. One click installs the node, the miner and the prover.

Devnet build. Coins have no value. The chain may reset.
IGNEUM / EMBERYour card becomes the network
01 / MiningIllustration
The card runs this hour’s program. A block comes out.Drawn, not live telemetry. The live graph is below.
GPU > BLOCK > PROOF > LOCK
Graphics cards onlyA new mining program every hour
The miners are the proversSame card, two jobs, 80/20 of every block
No premine, no stakeNo fee to any team in the protocol
Every claim has a statusThe evidence page says how far each one is tested
+
See what your card joins

Follow the work.
See the chain grow.

Open the observatory
BLOCKDAG/The live devnet, one lane per miner.
connecting
Live now
pending
pending vote keys mined a block in the last ten minutes; a card runs several
Proven and paid
pending
shards proven and paid on the chain so far, pending of them in the last ten minutes
The chip model
5x to 9x
Built for graphics cards. In our public model the strongest chip reaches 5x to 9x per joule against an RTX 5090 today; class v4, now on the vote, brings that to 2.1x to 3.9x, and class v5 makes the dataset the chain’s own state, so a chip that stores it or recomputes it is wrong on every item. The model and every measurement are public.
One node is read every 2 s. The chip figures are a cost model, not a measurement.Look a block up in the explorer
+
+
What miners asked for, 2018 to 2026

Three things, in order.

Twenty launch posts and threads, and the same three asks each time: hardware that keeps its value, income that does not fall off a cliff, a fair supply. The front page answers those three. Finality is on page two.

-
+
01

Your card stays a card.

The hash is a new random program every hour over a dataset the chain draws from its own state. No scheduled fork, no release a team must ship. The chip model and its number are published with every era on /evidence. When you stop, the card still games.

How mining works
-
+
02

Income with no cliff.

No halving day. The schedule glides month by month, then holds a steady share of supply for ever. A block pays its miner whether or not anyone buys a proof that day. What each card mines, and what its electricity costs, comes as one table when the testnet schedule is cut.

What your card does here
-
+
03

A fair supply.

No premine. No fund, no foundation, no fee to any team. Nobody holds a coin before block one. The founders mine from genesis with disclosed addresses and the same software as everyone else. The launch weeks ramp up from a low start, so they are worth less to a private farm.

@@ -194,195 +173,75 @@
Blocks are final by miners alone, with no stake and no other chain.How, on page two
- -
-
-
-
Live now
pending

pending vote keys mined a block in the last ten minutes; a card runs several.

-
Proven and paid
pending

shards proven and paid on the chain so far, pending of them in the last ten minutes.

-
The chip model
5x to 9x

Built for graphics cards. In our public model the strongest chip reaches 5x to 9x per joule against an RTX 5090 today; class v4, now on the vote, brings that to 2.1x to 3.9x, and class v5 makes the dataset the chain’s own state, so a chip that stores it or recomputes it is wrong on every item. The model and every measurement are public.

-
-

One node is read every 2 s. The chip figures are a cost model, not a measurement.

-
-
- -
-
-
-
See what your card joins

Follow the work.
See the chain grow.

- Open the observatory -
-
-
BLOCKDAG / the live devnet, one lane per miner
connecting to the devnet observer
-
- -
-
Every square is a real block, read from one node every 2 s. Nothing here is a replay.Look a block up in the explorer
-
-
- - -
-
-
-
-
The economic design, as published
-

Made for the people running the hardware.

-

Every block pays the card that found it and the cards that prove it. The protocol carves out nothing for a team, a foundation or a fund. No premine, no stake.

- -
-
-
Each block, by the rule
-
100%to miners and provers
-
-
-
80% Miningto the card that found the block
-
20% Provingto the cards that prove it
-
0% Treasurynothing to any team
-
-

The protocol rule, not an income forecast. Cap 4,000,000,000 IGN, halving every two years. Devnet coins have no value.

-
-
-
-
- -
-
-
-
Find your place

A whole network to look round.

-
-
-

Keep what you earned.

A desktop wallet that checks finality itself. Block rewards and proving payouts in one history.

The wallet
-

See what your card does here.

Measured hash rates per card, with the watts, the date and the log entry behind each number.

The bench table
-

Understand the design.

Mining, proving, finality, the economics and the open questions, in one readable paper.

Read the litepaper
-
-
-
- -
-
-
-
Igneum Ember · one click

Install. Start. The card mines.

- Everything about the miner -
- -

Devnet. Coins have no value and the chain may be reset.

-

Public testnet: not yet open; the devnet build is here for people who want to look. The public testnet is weeks away: three seed nodes and the public RPC are up, and it opens when the go checklist closes.

-

Read more about the miner. The protocol carries no fee. The one payment to the project is the Ember software’s optional 1% dev fee, like other GPU miners, off with one flag. Download only from this domain. Nobody from Igneum will ask for your seed. Questions: the Discord.

-
-
- -
-
-
-
-
Your next block starts here
-

Give your GPU something to do.

-

Download Ember, watch the devnet, and tell us what breaks. The people on the Discord run cards like yours.

-
-
- -

Experimental software. No promise of earnings.

-
-
-
-
- -
-
-
-

Every criticism, answered or conceded, and what Igneum does not claim.

- The ledger -
-
-
+
The economic design, as published

Made for the people running the hardware.

Every block pays the card that found it and the cards that prove it. The protocol carves out nothing for a team, a foundation or a fund. No premine, no stake.

Each block, by the rule
100%to miners and provers
80% Miningto the card that found the block
20% Provingto the cards that prove it
0% Treasurynothing to any team

The protocol rule, not an income forecast. Cap 4,000,000,000 IGN, halving every two years. Devnet coins have no value.

+
Find your place

A whole network to look round.

Keep what you earned.

A desktop wallet that checks finality itself. Block rewards and proving payouts in one history.

The wallet

See what your card does here.

Measured hash rates per card, with the watts, the date and the log entry behind each number.

The bench table

Understand the design.

Mining, proving, finality, the economics and the open questions, in one readable paper.

Read the litepaper
+
Igneum Ember · one click

Install. Start. The card mines.

Everything about the miner

Devnet. Coins have no value and the chain may be reset.

Public testnet: not yet open; the devnet build is here for people who want to look. The public testnet is weeks away: three seed nodes and the public RPC are up, and it opens when the go checklist closes.

Read more about the miner. The protocol carries no fee. The one payment to the project is the Ember software’s optional 1% dev fee, like other GPU miners, off with one flag. Download only from this domain. Nobody from Igneum will ask for your seed. Questions: the Discord.

+
Your next block starts here

Give your GPU
something to do.

Download Ember, watch the devnet, and tell us what breaks. The people on the Discord run cards like yours.

Experimental software. No promise of earnings.

+

Every criticism, answered or conceded, and what Igneum does not claim.

The ledger
- @@ -401,7 +260,7 @@ function fmtHash(h){if(h===null||h===undefined||!(h>0))return 'pending';return h>=1e9?(h/1e9).toFixed(1)+' GH/s':(h/1e6).toFixed(1)+' MH/s';} var cap=document.getElementById('steps-caption'),st=document.getElementById('dag-state'); // the feed's state is the caption while the feed is not live; the scene writes the caption while it is - function onState(s,r){st.className='state '+(s==='live'?'live':s==='failed'?'bad':'');st.innerHTML=''+(s==='live'?'live devnet':s==='connecting'?'connecting to the devnet observer':r||s); + function onState(s,r){st.className='pill'+(s==='live'?' warm':'');st.textContent=(s==='live'?'live devnet':s==='connecting'?'connecting':r||s); if(s==='live')return;cap.classList.add('muted');cap.textContent=s==='connecting'?'Connecting to the devnet observer.':s==='stale'?(r.charAt(0).toUpperCase()+r.slice(1))+'. The scene holds the last stored blocks.':'Live feed unavailable. The scene holds its last blocks; the live page shows the last stored state.';} function onData(d){var s=d.state,pv=d.proving; document.getElementById('f-hash').textContent=fmtHash(s.hashes_per_second_estimate); diff --git a/site/journey.html b/site/journey.html index 14b608be..e2896c28 100644 --- a/site/journey.html +++ b/site/journey.html @@ -31,6 +31,7 @@ + \ No newline at end of file diff --git a/site/partials/nav.html b/site/partials/nav.html index c24e6b4e..5b195f3a 100644 --- a/site/partials/nav.html +++ b/site/partials/nav.html @@ -1,9 +1,10 @@ diff --git a/site/provenance.html b/site/provenance.html index 9bf13d1c..500f792b 100644 --- a/site/provenance.html +++ b/site/provenance.html @@ -31,6 +31,7 @@ +

${r} at ${width}px: the package (left) and the served page (right)

package
served
`; + const tmp = join(out, `${name}-sheet.html`); writeFileSync(tmp, html); + await page.setViewportSize({ width: width * 2 + 72, height: 900 }); + await page.goto(pathToFileURL(tmp).href, { waitUntil: 'load' }); await page.waitForTimeout(400); + try { await page.screenshot({ path: sheet, fullPage: true, timeout: 30000 }); } catch (e) { console.error(`${r} sheet: ${e.message}`); } + await page.setViewportSize({ width, height: 900 }); + sheets.push(sheet); console.log(sheet); +} +await browser.close(); +console.log(`${sheets.length} sheets in ${out}`); From b94a3d1cbf17e74e2076ce3bda2082e039399be1 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:16:17 +0000 Subject: [PATCH 11/12] Worker dashboard: only a build-* slot held without a line shows as a "held, no holder line" card (the measure lock is not a build) Co-Authored-By: Claude Fable 5.1 --- tools/workers/page/workers.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/workers/page/workers.html b/tools/workers/page/workers.html index 4d103794..c8cd8333 100644 --- a/tools/workers/page/workers.html +++ b/tools/workers/page/workers.html @@ -245,7 +245,7 @@ function renderCrew() { function nowItems() { const out = []; for (const bx of boxesOf(D)) for (const r of bx.running || []) out.push({ where: bx.name, slot: r.slot, title: `${kindLabel(r.kind)}`, sub: `${r.worktree || '?'}/${r.crate || ''}${r.agent ? ' · ' + r.agent : ''}`, cmd: r.command, started_at: r.started_at, kind: r.kind, extra: [r.target || '', classLine(r)].filter(Boolean).join(' · ') }); - for (const bx of boxesOf(D)) for (const h of (bx.slots && bx.slots.held || []).filter(h => h.no_line && !(bx.running || []).some(r => r.slot === h.slot))) out.push({ where: bx.name, slot: h.slot, title: 'held, no holder line', sub: 'the slot is flock-held but its file is empty (the holder wrote no line)', cmd: '', started_at: null, kind: null, extra: '' }); + for (const bx of boxesOf(D)) for (const h of (bx.slots && bx.slots.held || []).filter(h => h.no_line && /^build-\d+$/.test(h.slot) && !(bx.running || []).some(r => r.slot === h.slot))) out.push({ where: bx.name, slot: h.slot, title: 'held, no holder line', sub: 'the slot is flock-held but its file is empty (the holder wrote no line)', cmd: '', started_at: null, kind: null, extra: '' }); for (const h of (D.mac && D.mac.slots.held) || []) out.push({ where: D.mac.name, slot: h.slot, title: h.slot === 'measure' ? 'measurement' : h.slot.startsWith('run') ? 'functional run' : kindLabel(h.kind), sub: h.worktree || 'this Mac', cmd: h.command || h.label, started_at: h.since, kind: h.kind, extra: h.waited_s ? `waited ${fmtDur(h.waited_s)}` : '' }); for (const pc of (D.pcs && D.pcs.machines) || []) if (pc.running) out.push({ where: pc.name, slot: 'relay job', title: `${pc.running.kind} ${pc.running.job}`, sub: pc.running.title || pc.machine || '', cmd: pc.running.stage ? `stage ${pc.running.stage}` : 'reports every 5 min while it runs', started_at: pc.running.started_at, kind: pc.running.kind, extra: pc.running.last_report_at ? `last report ${ago(pc.running.last_report_at)}` : '' }); return out; From 97255a4e8fec5dc9f14ad3eeb80a12d3fd26d17f Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:22:09 +0000 Subject: [PATCH 12/12] Build boxes: the slot holder keeps its own line (a keeper, with its self-test in the gate); an explicit --jobs is clamped for bounded classes; one git remote per box; the host-file double suffix The dashboard lane, 7 October 2026 10:39Z: both slots of build-1 flock-held and EMPTY while two suites ran. Cause: a run from a worktree without last night's append-mode fix opens a busy sibling's slot file with > on every probe. The holder now keeps its own line: a keeper re-writes it within BR_KEEP_S (20 s) whenever the file is empty, until release; remote-run.sh --self-test-keeper (in the gate) truncates a held line and sees it return, and sees nothing written after release; live on build-1 at 11:19Z (the line came back in 25 s). The first version deadlocked the runner's bare wait with the keeper (build-2's first run hung 15 min after its test passed): the keeper stops before the wait. The two running suites' -j 90 came from explicit --jobs 90: a bounded class now clamps it to its cap with a log line (pass --priority gate for the full set). run-from-mac.sh --box N: the host file was suffixed twice (build-server-2-2) and every box's mirror would have shared one remote name; one remote per box (build-N). build-2's first green run: a suite at nice 10 on 32 cores, jobs 32, 986 s cold. Co-Authored-By: Claude Fable 5.1 --- infra/build-server/remote-run.sh | 30 +++++++++++++++++++++++++++++- infra/build-server/run-from-mac.sh | 20 +++++++++++--------- tools/build-remote.sh | 3 +++ tools/ci/pre-push.sh | 1 + 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/infra/build-server/remote-run.sh b/infra/build-server/remote-run.sh index 0190a31a..15facd9f 100755 --- a/infra/build-server/remote-run.sh +++ b/infra/build-server/remote-run.sh @@ -110,6 +110,21 @@ checkout_tree() { set +e } +if [ "${1:-}" = --self-test-keeper ]; then + # the keeper restores a truncated holder line within its interval, and stops at release + t=$(mktemp -d); trap 'rm -rf "$t"' EXIT + BR_PID=$$; BR_LABEL="keeper self-test"; got=0; waited=3; SLOTS_DIR="$t"; BR_KEEP_S=1 + holder_line() { printf 'pid %s since %sZ waited %s s: %s\n' "$BR_PID" "$(date -u +%H:%M:%S)" "$1" "$BR_LABEL"; } + holder_line "$waited" > "$t/build-0" + keeper_pid=""; keep_line() { local f="$1" w="$2"; ( while kill -0 "$BR_PID" 2>/dev/null; do [ -s "$f" ] || holder_line "$w" > "$f" 2>/dev/null; sleep "${BR_KEEP_S:-20}"; done ) & keeper_pid=$!; } + keep_line "$t/build-0" "$waited" + : > "$t/build-0"; sleep 2.5 + grep -q "^pid $$ since .* waited 3 s: keeper self-test$" "$t/build-0" || { echo "keeper self-test: the truncated holder line was NOT restored"; kill "$keeper_pid" 2>/dev/null; exit 1; } + kill "$keeper_pid" 2>/dev/null; wait "$keeper_pid" 2>/dev/null; : > "$t/build-0"; sleep 2.5 + [ ! -s "$t/build-0" ] || { echo "keeper self-test: the keeper kept writing after release"; exit 1; } + echo "keeper self-test: a truncated holder line comes back within the interval; nothing is written after release"; exit 0 +fi + if [ "${1:-}" = --self-test ]; then t=$(mktemp -d); trap 'rm -rf "$t"' EXIT git init -q --bare -b master "$t/mirror.git" @@ -369,7 +384,17 @@ else echo "build-remote: holding build-$got on $BR_HOST (waited $waited s; $held of $slots slots held, CARGO_BUILD_JOBS=$BR_JOBS, kind ${BR_KIND:-other}, nice ${BR_NICE:-0}, cores $( [ "${BR_CORES:-0}" = 0 ] && nproc || echo "$BR_CORES"))" >&2 fi -release_slot() { if [ "$got" = measure ]; then : > "$SLOTS_DIR/measure"; else : > "$SLOTS_DIR/build-$got"; fi; } +# The holder keeps its own line (the watcher-trust rule, 7 October 2026 10:39Z: two held slots read EMPTY while two suites ran, +# because a run from a worktree without last night's append-mode fix still opens a busy sibling's slot file with `>` on every +# probe). A keeper re-writes the holder line whenever it finds the file empty, every BR_KEEP_S seconds, until release_slot. +keeper_pid="" +keep_line() { # + local f="$1" w="$2" + ( while kill -0 "$BR_PID" 2>/dev/null; do [ -s "$f" ] || holder_line "$w" > "$f" 2>/dev/null; sleep "${BR_KEEP_S:-20}"; done ) & + keeper_pid=$! +} +if [ "$got" = measure ]; then keep_line "$SLOTS_DIR/measure" "$waited"; else keep_line "$SLOTS_DIR/build-$got" "$waited"; fi +release_slot() { [ -n "$keeper_pid" ] && { kill "$keeper_pid" 2>/dev/null; wait "$keeper_pid" 2>/dev/null; keeper_pid=""; }; if [ "$got" = measure ]; then : > "$SLOTS_DIR/measure"; else : > "$SLOTS_DIR/build-$got"; fi; } cd "$BR_DIR" || { BR_CLASS=no-dir jsonlog 2 "$got" "$waited" "" "$(date +%s)" "" "" "" "" "" ""; redlog 2 0 no-dir; release_slot; exit 2; } # PRE-FLIGHT for a cargo command (the instant-death class, 6 October 2026): the manifest parses and every -p package exists, @@ -425,6 +450,9 @@ ncpu=$(nproc); cores_str="0-$((ncpu - 1))" if [ "${BR_CORES:-0}" -gt 0 ] && [ "${BR_CORES}" -lt "$ncpu" ]; then cores_str="$((ncpu - BR_CORES))-$((ncpu - 1))"; fi ( [ "${BR_NICE:-0}" -gt 0 ] && renice -n "$BR_NICE" -p $BASHPID >/dev/null 2>&1; [ "$cores_str" != "0-$((ncpu - 1))" ] && taskset -cp "$cores_str" $BASHPID >/dev/null 2>&1; eval "$BR_CMD" ) > >(tee -a "$BR_RUN_LOG") 2> >(tee -a "$BR_RUN_LOG" >&2) rc=$? +# the keeper stops BEFORE the bare `wait` (which flushes the two tees): a bare wait also waits for the keeper, and the keeper waits +# for this script, a deadlock that held build-2's first run 15 minutes after its test had passed (7 Oct 2026, 11:04 to 11:19Z) +if [ -n "$keeper_pid" ]; then kill "$keeper_pid" 2>/dev/null; wait "$keeper_pid" 2>/dev/null; keeper_pid=""; fi wait t2=$(date +%s); secs=$(( t2 - t1 )) exec_after=$(stat_field "Compile requests executed"); hits_after=$(stat_field "Cache hits "); misses_after=$(stat_field "Cache misses ") diff --git a/infra/build-server/run-from-mac.sh b/infra/build-server/run-from-mac.sh index 4c421974..a5a275a8 100755 --- a/infra/build-server/run-from-mac.sh +++ b/infra/build-server/run-from-mac.sh @@ -27,7 +27,9 @@ WIRE_ONLY=0; [ "${1:-}" = --wire-only ] && WIRE_ONLY=1 # box N is igneum-build-N with its own dashboard feed name build-N.igneum.network (the dashboard lane's collector reads one server # section per box; main adds the A record with the IP) [ "$BOX" = 1 ] || { BOX_HOSTNAME="${BOX_HOSTNAME:-igneum-build-$BOX}"; WORKERS_HOST="${WORKERS_HOST:-build-$BOX.igneum.network}"; export BOX_HOSTNAME WORKERS_HOST; } -BS_HOST_FILE=$(bs_box_file "$BOX"); export BS_BOX="$BOX" # lib.sh's bs_host reads BS_BOX +export BS_BOX="$BOX" # lib.sh's bs_host derives the host file from BS_BOX (box 1: build-server; box N: build-server-N); never set BS_HOST_FILE here (7 Oct 2026: a second suffix, build-server-2-2) +HOST_FILE=$(bs_box_file "$BOX") +REMOTE="build"; [ "$BOX" = 1 ] || REMOTE="build-$BOX" # one git remote per box in the two repositories # the toolchain pin travels from rust-toolchain.toml unless RUST_TOOLCHAIN is set by hand (one file pins every side, 7 Oct 2026) [ -n "${RUST_TOOLCHAIN:-}" ] || RUST_TOOLCHAIN=$(sed -n 's/^channel *= *"\([^"]*\)".*/\1/p' "$REPO/rust-toolchain.toml" 2>/dev/null | head -1); export RUST_TOOLCHAIN PASS="" # a string, not an array: bash 3.2 (the Mac) treats an empty array as unbound under set -u @@ -48,19 +50,19 @@ if [ "$WIRE_ONLY" = 0 ]; then if "${ROOT_SSH[@]}" 'hostname' 2>/dev/null | grep -q '^rescue'; then bs_die "still in the rescue system after provision.sh"; fi fi -mkdir -p "$(dirname "$BS_HOST_FILE")" -printf 'build@%s\n' "$IP" > "$BS_HOST_FILE" -bs_log "wrote $BS_HOST_FILE" +mkdir -p "$(dirname "$HOST_FILE")" +printf 'build@%s\n' "$IP" > "$HOST_FILE" +bs_log "wrote $HOST_FILE" bs_host bs_ssh 'hostname; nproc' >/dev/null || bs_die "build@$IP does not answer with $BS_KEY" wire_remote() { # repo-dir mirror label local dir="$1" mirror="$2" label="$3" url="$BS_HOST:$2" cur - cur=$(git -C "$dir" remote get-url build 2>/dev/null || true) - if [ -z "$cur" ]; then git -C "$dir" remote add build "$url"; bs_log "$label: remote build = $url" - elif [ "$cur" != "$url" ]; then git -C "$dir" remote set-url build "$url"; bs_log "$label: remote build -> $url" - else bs_log "$label: remote build ok"; fi - GIT_SSH_COMMAND="$BS_SSH_CMD" git -C "$dir" push -q --force build --all && bs_log "$label: every branch pushed to $mirror ($(git -C "$dir" branch --list | wc -l | tr -d ' ') branches)" || bs_die "$label: push to $mirror failed" + cur=$(git -C "$dir" remote get-url "$REMOTE" 2>/dev/null || true) + if [ -z "$cur" ]; then git -C "$dir" remote add "$REMOTE" "$url"; bs_log "$label: remote $REMOTE = $url" + elif [ "$cur" != "$url" ]; then git -C "$dir" remote set-url "$REMOTE" "$url"; bs_log "$label: remote $REMOTE -> $url" + else bs_log "$label: remote $REMOTE ok"; fi + GIT_SSH_COMMAND="$BS_SSH_CMD" git -C "$dir" push -q --force "$REMOTE" --all && bs_log "$label: every branch pushed to $mirror ($(git -C "$dir" branch --list | wc -l | tr -d ' ') branches)" || bs_die "$label: push to $mirror failed" } wire_remote "$MAIN_REPO" "$BS_MIRROR_REPO" "igneum" wire_remote "$MAIN_REPO/vendor/igneum-node" "$BS_MIRROR_NODE" "igneum-node (the fork)" diff --git a/tools/build-remote.sh b/tools/build-remote.sh index 05e40320..9618ec3d 100755 --- a/tools/build-remote.sh +++ b/tools/build-remote.sh @@ -111,6 +111,9 @@ case "${CARGO_ARGS[0]:-build}" in esac if [ "$PRIORITY" = gate ]; then BR_NICE=0; BR_CORES=0; BR_JOBS_CAP=0 elif [ "$SCHED_CLASS" = suite ] || [ "$SCHED_CLASS" = bench ]; then BR_NICE=10; BR_CORES=32; BR_JOBS_CAP=32; fi +# an explicit --jobs above the bounded class's cap is clamped (main's rule: bounded unless a priority flag; 7 Oct 2026: two suites +# ran at -j 90 on a box at load 190 because their callers passed --jobs 90) +if [ "$BR_JOBS_CAP" -gt 0 ] && [ -n "$JOBS" ] && [ "$JOBS" -gt "$BR_JOBS_CAP" ]; then bs_log "--jobs $JOBS clamped to $BR_JOBS_CAP for a $SCHED_CLASS (pass --priority gate for the full set)"; JOBS=$BR_JOBS_CAP; fi export BR_NICE BR_CORES BR_JOBS_CAP BR_PRIORITY if [ "$PLAN" = 1 ]; then k="$SCHED_CLASS"; [ "$PRIORITY" = gate ] && k=gate diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index 7db09789..659e9839 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -72,6 +72,7 @@ tree_checks() { run "root prover playbooks kill the GPU server and unlink its socket" bash tools/ci/prover-socket-check.sh run "commit-string gate self-test" bash tools/ci/commit-string-check.sh --self-test run "build server remote checkout self-test" bash infra/build-server/remote-run.sh --self-test + run "a slot holder keeps its own line for the whole run (the watcher-trust rule)" bash infra/build-server/remote-run.sh --self-test-keeper run "the remote checkout resets the mirror's tree before the branch checkout (the stale-overlay class)" bash -c 'bash tools/ci/mirror-reset-check.sh --self-test && bash tools/ci/mirror-reset-check.sh' run "the remote checkout's clean spares a lane's scratch (.igneum-scratch-spare, the fixed prefixes, never -x; the lost-scratch class)" bash -c 'bash tools/ci/scratch-spare-check.sh --self-test && bash tools/ci/scratch-spare-check.sh' run "long-running tools keep their body in one parsed block (the edited-while-running class)" bash -c 'bash tools/ci/whole-body-check.sh --self-test && bash tools/ci/whole-body-check.sh'