Merge master 0ffae7018 into lab-20261009 under the master-landing lock
This commit is contained in:
commit
bc360df44f
11 changed files with 204 additions and 35 deletions
|
|
@ -50,3 +50,14 @@ build-1:/srv/queue/build-queue.md (the reader reads that one; this file is its s
|
|||
|
||||
A lane starts its entry by writing its pid file (`<pid> <start UTC> <label>`, the format of the slot files) under /srv/queue/pids on
|
||||
build-1 (or on its own box, mirrored there by the lane) and ends it by removing the file and replacing the entry.
|
||||
|
||||
## Fault lines (build-1's guards)
|
||||
|
||||
- 9 October 2026, 11:32 UK (the network watch): the memory guard stopped the devnet-4 seed and hand by their units as "a second fresh sync"
|
||||
while all three were synced. The reference tip feed's Mac-side writer (hub1-tip-feed.sh) had died with the lanes at the founder's stop, so
|
||||
/srv/canary/hub1-tip.txt froze at 29,482 for thirteen minutes while the three live nodes read more than 100 past it; the guard counted
|
||||
three fresh syncs and kept the earliest. /api/live was stale for nine minutes. Fix (landed from this line): the guard reads the feed's age
|
||||
before any height compare (a feed older than three minutes is a FEED FAULT line, once per ten minutes, never a sync count); a unit counts
|
||||
only on two consecutive passes (one backward feed sample or one timed-out read is never a count); a unit whose datadir's oldest file is
|
||||
older than 30 minutes is a resume, never a fresh sync, never stopped. The feed writer runs under its pid file on the Mac and the network
|
||||
watch keeps it alive; the guard's one-fresh-sync rule itself stands.
|
||||
|
|
|
|||
|
|
@ -20434,7 +20434,7 @@
|
|||
"plan_status": "NOT RUN",
|
||||
"run_id": "lab-20261009-01",
|
||||
"evidence_path": "docs/analysis/class-v6/1p5x/lab/README.md; docs/analysis/class-v6/1p5x/lab/table.md",
|
||||
"updated": "2026-10-09T11:02:10.662Z",
|
||||
"updated": "2026-10-09T11:09:03.339Z",
|
||||
"evidence_record": {
|
||||
"requirement_id": "R15-01",
|
||||
"decision": "NOT RUN",
|
||||
|
|
@ -20455,7 +20455,7 @@
|
|||
},
|
||||
"claim_impact": "none",
|
||||
"reviewer": "",
|
||||
"at": "2026-10-09T11:02:10.662Z",
|
||||
"at": "2026-10-09T11:09:03.339Z",
|
||||
"note": "The lab's interface and the candidate table at the fan-out: the frozen v6 control carried from the record (the house 5090 at its knee 4,011 nJ per hash, board power; the X0 amendment 5b rows: the DRAM board 2.35x same node WITNESS, the N2 die 7.8x the worst credible), the cohort rows for the 3090, the 5080 and the AMD and Intel tiers NOT RUN, adversarial set v1 pending; the three other controls (ProgPoW 0.9.4 reference, FishHash, the cooperative ARX candidate) NOT RUN with their clocks. No score is the lab's until the cohort and the adversary rows are in; R15-01, R15-08 and R15-09 stay NOT RUN until read. X10 (cand-x10) joins the table NOT RUN with its per-lane assignments and clocks; COHORT's ctl-v6 denominator set is in flight on eight rented pods. 12:1x UK: adversarial set v1 on the table (the frozen v6's same-node board 2.57x, 2.04 to 3.00, the fused tail CLAIMED until the routed lanes land; the die 11.4x as a coexistence row); the coexistence table (interface section 8b) added as the scoreboard's headline; SEARCH's first screen recorded as SCREEN rejections. Still NOT RUN: no cohort score until COHORT's eight rows are read back. | The 1.5x adversarial comparison laboratory (the founder's order, 9 October 2026, 11:4x UK): one evaluation for every control and candidate (the measurement recipe with the read-only twin for the memory share, the adversary interface returning the lowest credible specialist energy per design as a complete machine with WITNESS or BOUND labels, the score as the worst comparison over the required cohort same node and one node ahead, the rejection filters F1 to F6, the regression set REG-01 to REG-08 from the 8 and 9 October failures). Every row NOT RUN until the panel reads it; the first measured score (the frozen v6 control on the cohort against adversarial set v1) lands as a later batch under this cell. Amended 11:5x UK: X10, the full-sector fold candidate of the founder's third review, added as cand-x10 with the scoped same-node board claim (the die and node-ahead chips as coexistence rows); the AMD and Intel tiers NOT RUN on every candidate (no rented card; the house rigs unreachable by software) until a card is found."
|
||||
},
|
||||
"evidence_records": {
|
||||
|
|
@ -20479,7 +20479,7 @@
|
|||
},
|
||||
"claim_impact": "none",
|
||||
"reviewer": "",
|
||||
"at": "2026-10-09T11:02:10.662Z",
|
||||
"at": "2026-10-09T11:09:03.339Z",
|
||||
"note": "The lab's interface and the candidate table at the fan-out: the frozen v6 control carried from the record (the house 5090 at its knee 4,011 nJ per hash, board power; the X0 amendment 5b rows: the DRAM board 2.35x same node WITNESS, the N2 die 7.8x the worst credible), the cohort rows for the 3090, the 5080 and the AMD and Intel tiers NOT RUN, adversarial set v1 pending; the three other controls (ProgPoW 0.9.4 reference, FishHash, the cooperative ARX candidate) NOT RUN with their clocks. No score is the lab's until the cohort and the adversary rows are in; R15-01, R15-08 and R15-09 stay NOT RUN until read. X10 (cand-x10) joins the table NOT RUN with its per-lane assignments and clocks; COHORT's ctl-v6 denominator set is in flight on eight rented pods. 12:1x UK: adversarial set v1 on the table (the frozen v6's same-node board 2.57x, 2.04 to 3.00, the fused tail CLAIMED until the routed lanes land; the die 11.4x as a coexistence row); the coexistence table (interface section 8b) added as the scoreboard's headline; SEARCH's first screen recorded as SCREEN rejections. Still NOT RUN: no cohort score until COHORT's eight rows are read back. | The 1.5x adversarial comparison laboratory (the founder's order, 9 October 2026, 11:4x UK): one evaluation for every control and candidate (the measurement recipe with the read-only twin for the memory share, the adversary interface returning the lowest credible specialist energy per design as a complete machine with WITNESS or BOUND labels, the score as the worst comparison over the required cohort same node and one node ahead, the rejection filters F1 to F6, the regression set REG-01 to REG-08 from the 8 and 9 October failures). Every row NOT RUN until the panel reads it; the first measured score (the frozen v6 control on the cohort against adversarial set v1) lands as a later batch under this cell. Amended 11:5x UK: X10, the full-sector fold candidate of the founder's third review, added as cand-x10 with the scoped same-node board claim (the die and node-ahead chips as coexistence rows); the AMD and Intel tiers NOT RUN on every candidate (no rented card; the house rigs unreachable by software) until a card is found."
|
||||
}
|
||||
},
|
||||
|
|
@ -21144,7 +21144,7 @@
|
|||
"plan_status": "NOT RUN",
|
||||
"run_id": "lab-20261009-01",
|
||||
"evidence_path": "docs/analysis/class-v6/1p5x/lab/README.md; docs/analysis/class-v6/1p5x/lab/table.md",
|
||||
"updated": "2026-10-09T11:02:10.662Z",
|
||||
"updated": "2026-10-09T11:09:03.339Z",
|
||||
"evidence_record": {
|
||||
"requirement_id": "R15-08",
|
||||
"decision": "NOT RUN",
|
||||
|
|
@ -21165,7 +21165,7 @@
|
|||
},
|
||||
"claim_impact": "none",
|
||||
"reviewer": "",
|
||||
"at": "2026-10-09T11:02:10.662Z",
|
||||
"at": "2026-10-09T11:09:03.339Z",
|
||||
"note": "The lab's interface and the candidate table at the fan-out: the frozen v6 control carried from the record (the house 5090 at its knee 4,011 nJ per hash, board power; the X0 amendment 5b rows: the DRAM board 2.35x same node WITNESS, the N2 die 7.8x the worst credible), the cohort rows for the 3090, the 5080 and the AMD and Intel tiers NOT RUN, adversarial set v1 pending; the three other controls (ProgPoW 0.9.4 reference, FishHash, the cooperative ARX candidate) NOT RUN with their clocks. No score is the lab's until the cohort and the adversary rows are in; R15-01, R15-08 and R15-09 stay NOT RUN until read. X10 (cand-x10) joins the table NOT RUN with its per-lane assignments and clocks; COHORT's ctl-v6 denominator set is in flight on eight rented pods. 12:1x UK: adversarial set v1 on the table (the frozen v6's same-node board 2.57x, 2.04 to 3.00, the fused tail CLAIMED until the routed lanes land; the die 11.4x as a coexistence row); the coexistence table (interface section 8b) added as the scoreboard's headline; SEARCH's first screen recorded as SCREEN rejections. Still NOT RUN: no cohort score until COHORT's eight rows are read back. | The 1.5x adversarial comparison laboratory (the founder's order, 9 October 2026, 11:4x UK): one evaluation for every control and candidate (the measurement recipe with the read-only twin for the memory share, the adversary interface returning the lowest credible specialist energy per design as a complete machine with WITNESS or BOUND labels, the score as the worst comparison over the required cohort same node and one node ahead, the rejection filters F1 to F6, the regression set REG-01 to REG-08 from the 8 and 9 October failures). Every row NOT RUN until the panel reads it; the first measured score (the frozen v6 control on the cohort against adversarial set v1) lands as a later batch under this cell. Amended 11:5x UK: X10, the full-sector fold candidate of the founder's third review, added as cand-x10 with the scoped same-node board claim (the die and node-ahead chips as coexistence rows); the AMD and Intel tiers NOT RUN on every candidate (no rented card; the house rigs unreachable by software) until a card is found."
|
||||
},
|
||||
"evidence_records": {
|
||||
|
|
@ -21189,7 +21189,7 @@
|
|||
},
|
||||
"claim_impact": "none",
|
||||
"reviewer": "",
|
||||
"at": "2026-10-09T11:02:10.662Z",
|
||||
"at": "2026-10-09T11:09:03.339Z",
|
||||
"note": "The lab's interface and the candidate table at the fan-out: the frozen v6 control carried from the record (the house 5090 at its knee 4,011 nJ per hash, board power; the X0 amendment 5b rows: the DRAM board 2.35x same node WITNESS, the N2 die 7.8x the worst credible), the cohort rows for the 3090, the 5080 and the AMD and Intel tiers NOT RUN, adversarial set v1 pending; the three other controls (ProgPoW 0.9.4 reference, FishHash, the cooperative ARX candidate) NOT RUN with their clocks. No score is the lab's until the cohort and the adversary rows are in; R15-01, R15-08 and R15-09 stay NOT RUN until read. X10 (cand-x10) joins the table NOT RUN with its per-lane assignments and clocks; COHORT's ctl-v6 denominator set is in flight on eight rented pods. 12:1x UK: adversarial set v1 on the table (the frozen v6's same-node board 2.57x, 2.04 to 3.00, the fused tail CLAIMED until the routed lanes land; the die 11.4x as a coexistence row); the coexistence table (interface section 8b) added as the scoreboard's headline; SEARCH's first screen recorded as SCREEN rejections. Still NOT RUN: no cohort score until COHORT's eight rows are read back. | The 1.5x adversarial comparison laboratory (the founder's order, 9 October 2026, 11:4x UK): one evaluation for every control and candidate (the measurement recipe with the read-only twin for the memory share, the adversary interface returning the lowest credible specialist energy per design as a complete machine with WITNESS or BOUND labels, the score as the worst comparison over the required cohort same node and one node ahead, the rejection filters F1 to F6, the regression set REG-01 to REG-08 from the 8 and 9 October failures). Every row NOT RUN until the panel reads it; the first measured score (the frozen v6 control on the cohort against adversarial set v1) lands as a later batch under this cell. Amended 11:5x UK: X10, the full-sector fold candidate of the founder's third review, added as cand-x10 with the scoped same-node board claim (the die and node-ahead chips as coexistence rows); the AMD and Intel tiers NOT RUN on every candidate (no rented card; the house rigs unreachable by software) until a card is found."
|
||||
}
|
||||
},
|
||||
|
|
@ -21234,7 +21234,7 @@
|
|||
"plan_status": "NOT RUN",
|
||||
"run_id": "lab-20261009-01",
|
||||
"evidence_path": "docs/analysis/class-v6/1p5x/lab/README.md; docs/analysis/class-v6/1p5x/lab/table.md",
|
||||
"updated": "2026-10-09T11:02:10.662Z",
|
||||
"updated": "2026-10-09T11:09:03.339Z",
|
||||
"evidence_record": {
|
||||
"requirement_id": "R15-09",
|
||||
"decision": "NOT RUN",
|
||||
|
|
@ -21255,7 +21255,7 @@
|
|||
},
|
||||
"claim_impact": "none",
|
||||
"reviewer": "",
|
||||
"at": "2026-10-09T11:02:10.662Z",
|
||||
"at": "2026-10-09T11:09:03.339Z",
|
||||
"note": "The lab's interface and the candidate table at the fan-out: the frozen v6 control carried from the record (the house 5090 at its knee 4,011 nJ per hash, board power; the X0 amendment 5b rows: the DRAM board 2.35x same node WITNESS, the N2 die 7.8x the worst credible), the cohort rows for the 3090, the 5080 and the AMD and Intel tiers NOT RUN, adversarial set v1 pending; the three other controls (ProgPoW 0.9.4 reference, FishHash, the cooperative ARX candidate) NOT RUN with their clocks. No score is the lab's until the cohort and the adversary rows are in; R15-01, R15-08 and R15-09 stay NOT RUN until read. X10 (cand-x10) joins the table NOT RUN with its per-lane assignments and clocks; COHORT's ctl-v6 denominator set is in flight on eight rented pods. 12:1x UK: adversarial set v1 on the table (the frozen v6's same-node board 2.57x, 2.04 to 3.00, the fused tail CLAIMED until the routed lanes land; the die 11.4x as a coexistence row); the coexistence table (interface section 8b) added as the scoreboard's headline; SEARCH's first screen recorded as SCREEN rejections. Still NOT RUN: no cohort score until COHORT's eight rows are read back. | The 1.5x adversarial comparison laboratory (the founder's order, 9 October 2026, 11:4x UK): one evaluation for every control and candidate (the measurement recipe with the read-only twin for the memory share, the adversary interface returning the lowest credible specialist energy per design as a complete machine with WITNESS or BOUND labels, the score as the worst comparison over the required cohort same node and one node ahead, the rejection filters F1 to F6, the regression set REG-01 to REG-08 from the 8 and 9 October failures). Every row NOT RUN until the panel reads it; the first measured score (the frozen v6 control on the cohort against adversarial set v1) lands as a later batch under this cell. Amended 11:5x UK: X10, the full-sector fold candidate of the founder's third review, added as cand-x10 with the scoped same-node board claim (the die and node-ahead chips as coexistence rows); the AMD and Intel tiers NOT RUN on every candidate (no rented card; the house rigs unreachable by software) until a card is found."
|
||||
},
|
||||
"evidence_records": {
|
||||
|
|
@ -21279,7 +21279,7 @@
|
|||
},
|
||||
"claim_impact": "none",
|
||||
"reviewer": "",
|
||||
"at": "2026-10-09T11:02:10.662Z",
|
||||
"at": "2026-10-09T11:09:03.339Z",
|
||||
"note": "The lab's interface and the candidate table at the fan-out: the frozen v6 control carried from the record (the house 5090 at its knee 4,011 nJ per hash, board power; the X0 amendment 5b rows: the DRAM board 2.35x same node WITNESS, the N2 die 7.8x the worst credible), the cohort rows for the 3090, the 5080 and the AMD and Intel tiers NOT RUN, adversarial set v1 pending; the three other controls (ProgPoW 0.9.4 reference, FishHash, the cooperative ARX candidate) NOT RUN with their clocks. No score is the lab's until the cohort and the adversary rows are in; R15-01, R15-08 and R15-09 stay NOT RUN until read. X10 (cand-x10) joins the table NOT RUN with its per-lane assignments and clocks; COHORT's ctl-v6 denominator set is in flight on eight rented pods. 12:1x UK: adversarial set v1 on the table (the frozen v6's same-node board 2.57x, 2.04 to 3.00, the fused tail CLAIMED until the routed lanes land; the die 11.4x as a coexistence row); the coexistence table (interface section 8b) added as the scoreboard's headline; SEARCH's first screen recorded as SCREEN rejections. Still NOT RUN: no cohort score until COHORT's eight rows are read back. | The 1.5x adversarial comparison laboratory (the founder's order, 9 October 2026, 11:4x UK): one evaluation for every control and candidate (the measurement recipe with the read-only twin for the memory share, the adversary interface returning the lowest credible specialist energy per design as a complete machine with WITNESS or BOUND labels, the score as the worst comparison over the required cohort same node and one node ahead, the rejection filters F1 to F6, the regression set REG-01 to REG-08 from the 8 and 9 October failures). Every row NOT RUN until the panel reads it; the first measured score (the frozen v6 control on the cohort against adversarial set v1) lands as a later batch under this cell. Amended 11:5x UK: X10, the full-sector fold candidate of the founder's third review, added as cand-x10 with the scoped same-node board claim (the die and node-ahead chips as coexistence rows); the AMD and Intel tiers NOT RUN on every candidate (no rented card; the house rigs unreachable by software) until a card is found."
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,23 +6,52 @@
|
|||
# the refusal is written. The sequencer (dn4-sequence.sh in the root account's home) starts the next one at the previous one's within-ten.
|
||||
# 2. Free memory under 15 GB (MemAvailable) writes an ALERT line; the Mac-side relay sends it to main.
|
||||
# Lines go to /srv/queue/faults.log and /srv/queue/alerts.log (build-readable). No kill by name: units only.
|
||||
#
|
||||
# What counts as a fresh sync (the network watch's fix, 9 October 2026, 11:5x UK, after the guard stopped the seed and the hand at 11:32 UK):
|
||||
# - the feed is read for its AGE first: the writer (a Mac-side job) had died with the lanes at the founder's stop, the file froze at 29,482
|
||||
# for thirteen minutes, three live nodes read more than 100 past it and the guard counted them as three fresh syncs. A feed older than
|
||||
# three minutes is a FEED FAULT line (once per ten minutes) and no sync count happens on that pass;
|
||||
# - a unit counts only on two consecutive passes (a single backward sample of the feed, or one timed-out read, is never a count);
|
||||
# - a unit whose datadir is older than 30 minutes (its oldest file) is never a fresh sync and is never stopped, whatever its height.
|
||||
set -u
|
||||
Q=/srv/queue; mkdir -p "$Q"; now=$(date -u +%Y-%m-%dT%H:%M:%SZ); uk=$(TZ=Europe/London date +%H:%M)
|
||||
Q=/srv/queue; mkdir -p "$Q"; now=$(date -u +%Y-%m-%dT%H:%M:%SZ); uk=$(TZ=Europe/London date +%H:%M); ts=$(date +%s)
|
||||
height() { curl -s --max-time 5 -X POST -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' "http://127.0.0.1:$1" 2>/dev/null | python3 -c 'import json,sys; r=json.load(sys.stdin).get("result"); print(int(r,16) if r else -1)' 2>/dev/null || echo -1; }
|
||||
hub=$(cut -d' ' -f1 /srv/canary/hub1-tip.txt 2>/dev/null); hub=${hub:-0}
|
||||
syncing=(); for pair in igneum-dn4-seed:27810 igneum-dn4-hand:26870 igneum-light-reader:26881; do u=${pair%%:*}; p=${pair##*:}
|
||||
finish() { printf '%s %s avail=%sGB syncing=%s\n' "$now" "$uk" "$(awk '/MemAvailable/{print int($2/1024/1024)}' /proc/meminfo)" "$1" > "$Q/memory-guard.last"; }
|
||||
# the feed line is "<EVM height> <tip hash> <DAA> <UTC time>"; its age decides whether heights may be compared at all
|
||||
read -r hub _hash _daa at < /srv/canary/hub1-tip.txt 2>/dev/null || true; hub=${hub:-0}
|
||||
fts=$(date -d "${at:-}" +%s 2>/dev/null || echo 0); fage=$(( ts - fts ))
|
||||
if [ "$fts" = 0 ] || [ "$fage" -gt 180 ]; then
|
||||
mark=$(cat "$Q/memory-guard.feedfault" 2>/dev/null || echo 0)
|
||||
if [ $(( ts - mark )) -ge 600 ]; then echo "$now ($uk UK) FEED FAULT memory guard: hub feed age ${fage} s (last write ${at:-none}, height $hub); the writer is the Mac-side hub1-tip-feed.sh under its pid file; no sync count on a stale feed" | tee -a "$Q/faults.log" >> "$Q/alerts.log"; echo "$ts" > "$Q/memory-guard.feedfault"; fi
|
||||
: > "$Q/memory-guard.syncing"; finish "feed-stale"; exit 0
|
||||
fi
|
||||
# a feed read more than 100 below the last good one is a bad read (11:04 UK, 9 Oct: the feed read 7,866 for one pass and the synced seed
|
||||
# counted as 21,000 ahead, so the hand and the light-reader were stopped); the last good read is kept in /srv/queue/memory-guard.hub
|
||||
last=$(cat "$Q/memory-guard.hub" 2>/dev/null || echo 0); if [ "$hub" -lt $(( last - 100 )) ]; then echo "$now ($uk UK) NOTE memory guard: hub feed read $hub against the last good $last; ignored" >> "$Q/alerts.log"; finish "feed-backward"; exit 0; fi; echo "$hub" > "$Q/memory-guard.hub"
|
||||
appdir() { systemctl show -p ExecStart --value "$1" 2>/dev/null | grep -oE -- '--appdir=[^ ;]+' | head -n1 | cut -d= -f2; }
|
||||
oldest_s() { local o; o=$(find "$1" -type f -printf '%T@\n' 2>/dev/null | sort -n | head -n1 | cut -d. -f1); [ -n "$o" ] && echo $(( ts - o )) || echo 0; }
|
||||
prev=" $(cat "$Q/memory-guard.syncing" 2>/dev/null | tr '\n' ' ') "
|
||||
seen=(); syncing=()
|
||||
for pair in igneum-dn4-seed:27810 igneum-dn4-hand:26870 igneum-light-reader:26881; do u=${pair%%:*}; p=${pair##*:}
|
||||
systemctl is-active --quiet "$u" || continue; h=$(height "$p"); d=$(( hub - h ))
|
||||
# a fresh sync is thousands behind; the band is 100 blocks so a moving feed never reads a synced node as syncing, and a unit in its
|
||||
# first two minutes whose RPC has not answered yet is not counted (10:32 UK, 9 Oct: the seed's reopen restart was read as a second sync)
|
||||
# an RPC that does not answer is UNKNOWN, never "syncing" (11:04 UK, 9 Oct: a timed-out read of the synced seed under load counted it as
|
||||
# syncing and the light-reader, within ten, was stopped as a second sync); only a height more than 100 behind the feed counts
|
||||
if [ "$h" -ge 0 ] && { [ "$d" -gt 100 ] || [ "$d" -lt -100 ]; }; then syncing+=("$u"); fi
|
||||
# only a unit started in the last 30 minutes is a fresh sync the guard may stop; an older unit is never touched
|
||||
age=$(( ts - $(date -d "$(systemctl show -p ActiveEnterTimestamp --value "$u")" +%s 2>/dev/null || echo 0) ))
|
||||
# a kept datadir (its oldest file older than 30 minutes) is a resume, never a fresh sync, whatever the height reads
|
||||
dd=$(appdir "$u"); dage=$([ -n "$dd" ] && [ -d "$dd" ] && oldest_s "$dd" || echo 0)
|
||||
if [ "$h" -ge 0 ] && { [ "$d" -gt 100 ] || [ "$d" -lt -100 ]; } && [ "$age" -lt 1800 ] && [ "$dage" -lt 1800 ]; then
|
||||
seen+=("$u"); case "$prev" in *" $u "*) syncing+=("$u");; esac # counted only on the second consecutive pass
|
||||
fi
|
||||
done
|
||||
printf '%s\n' "${seen[@]:-}" | sed '/^$/d' > "$Q/memory-guard.syncing"
|
||||
if [ "${#syncing[@]}" -gt 1 ]; then
|
||||
# keep the earliest-started, stop the rest by unit
|
||||
keep=""; keepts=0; for u in "${syncing[@]}"; do ts=$(date -d "$(systemctl show -p ActiveEnterTimestamp --value "$u")" +%s 2>/dev/null || echo 0); [ "$keepts" = 0 ] || [ "$ts" -lt "$keepts" ] && { keep=$u; keepts=$ts; }; done
|
||||
for u in "${syncing[@]}"; do [ "$u" = "$keep" ] && continue; systemctl stop "$u"; echo "$now ($uk UK) REFUSED a second fresh sync: $u stopped by its unit while $keep is below within-ten of the hub feed ($hub); the box-1 rule" | tee -a "$Q/faults.log" >> "$Q/alerts.log"; done
|
||||
keep=""; keepts=0; for u in "${syncing[@]}"; do t=$(date -d "$(systemctl show -p ActiveEnterTimestamp --value "$u")" +%s 2>/dev/null || echo 0); [ "$keepts" = 0 ] || [ "$t" -lt "$keepts" ] && { keep=$u; keepts=$t; }; done
|
||||
for u in "${syncing[@]}"; do [ "$u" = "$keep" ] && continue; systemctl stop "$u"; echo "$now ($uk UK) REFUSED a second fresh sync: $u stopped by its unit while $keep is below within-ten of the hub feed ($hub, age ${fage} s, two passes); the box-1 rule" | tee -a "$Q/faults.log" >> "$Q/alerts.log"; done
|
||||
fi
|
||||
avail=$(awk '/MemAvailable/{print int($2/1024/1024)}' /proc/meminfo); used=$(free -g | awk '/Mem:/{print $3}')
|
||||
if [ "$avail" -lt 15 ]; then echo "$now ($uk UK) ALERT build-1 free memory ${avail} GB (used ${used} GB; syncing: ${syncing[*]:-none}); under the 15 GB line" | tee -a "$Q/faults.log" >> "$Q/alerts.log"; fi
|
||||
printf '%s %s avail=%sGB syncing=%s\n' "$now" "$uk" "$avail" "${syncing[*]:-none}" > "$Q/memory-guard.last"
|
||||
finish "${syncing[*]:-none}"
|
||||
|
|
|
|||
|
|
@ -150,6 +150,10 @@ if [ "$VERIFY_ONLY" != 1 ] && [ "$CANARY_GATED" = 1 ] && [ -z "${IGNEUM_CANARY_S
|
|||
KINDS=""; [ -n "$MAC" ] && KINDS="$KINDS mac"; [ -n "$WIN" ] && KINDS="$KINDS windows"
|
||||
# shellcheck disable=SC2086
|
||||
canary_guard "$RELEASE_SHA" $KINDS || exit 1
|
||||
# rule 38 (9 October 2026): the entry's artefact must carry its update manifest URL in a known token folder (a DMG built without
|
||||
# the download token read "the update check is disabled in this build" and would never update); the Windows payload zip beside
|
||||
# the installer (or IGNEUM_WIN_PAYLOAD) is what carries the Windows app's packaged config
|
||||
for a in $MAC $WIN; do bash "$TOOLS/ci/packed-update-check.sh" "$a" || { echo "publish-manifest: REFUSED by the packed update-path guard (rule 38; above): nothing written" >&2; exit 1; }; done
|
||||
NOTES="${NOTES:+$NOTES; }release: $RELEASE_SHA (fresh-install canary PASS)"
|
||||
fi
|
||||
if [ -n "$NODE_BIN" ] || [ -n "$NET_DIGEST" ]; then
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ print(" ".join(ks))' "$SRC/igneum-app-latest.json" 2>/dev/null || true)
|
|||
canary_gate "$sha" "the app manifest" $kinds || exit 1
|
||||
fi
|
||||
if [ -n "$HIVE" ] && [ -z "${IGNEUM_CANARY_SELF_TEST_SKIP:-}" ]; then canary_gate "$RELEASE_SHA" "the HiveOS package" hive || exit 1; fi
|
||||
if [ -n "$HIVE" ] && [ -z "${IGNEUM_CANARY_SELF_TEST_SKIP:-}" ]; then bash "$ROOT/tools/ci/packed-update-check.sh" "$HIVE" | scrub || { echo "publish-public: REFUSED by the packed update-path guard (rule 38; above)" >&2; exit 1; }; fi
|
||||
if [ "$DO_APP" = 1 ]; then log "app manifest -> dl/public/"; publish_manifest igneum-app-latest.json; fi
|
||||
if [ "$DO_WALLET" = 1 ]; then log "wallet manifest -> dl/public/"; publish_manifest igneum-wallet-latest.json; fi
|
||||
if [ -n "$HIVE" ]; then
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ the proving outcome ledger (review B F08): every claimed job ends in one outcome
|
|||
the fleet prover's task protection (V6-08): the backpressure gate, task sizing, shard ordering, preflight verdicts and the flow identity on known-failed-first cases
|
||||
the registry's evidence rules: a PASS names evidence that exists, a touched evidence file moves with its row, stale evidence never reads PASS, a run_status needs the approval (self-test)
|
||||
the kit ISA check: a distribution kit's binaries carry no AVX-512 encoding (the x86-64-v3 baseline; self-test with a fake objdump)
|
||||
rule 38: an entry's artefact carries its update manifest URL in a known token folder; a build whose update check is off never publishes (self-test)
|
||||
F02 (Review B): the proof-rule test bypass cannot reach a release build: an env read with no cfg guard or under a default feature is red; a release binary carrying the bypass string is red (self-test)
|
||||
the test map merges structurally at a landing: master's cells plus the branch's, minus what the branch removed and master left (self-test)
|
||||
the box-form hashed identity list hashes a path entry's login only, never a generic path word (self-test)
|
||||
|
|
|
|||
111
tools/ci/packed-update-check.sh
Executable file
111
tools/ci/packed-update-check.sh
Executable file
|
|
@ -0,0 +1,111 @@
|
|||
#!/usr/bin/env bash
|
||||
# Rule 38 (9 October 2026, 11:46 UK: the first 2.0.3 DMG built on a Mac without the download token read "the update check is
|
||||
# disabled in this build" and would have shipped an app that never updates, a founder's-hand fault under the no-manual-inputs
|
||||
# rule): no entry publishes whose app cannot find its update manifest. The guard opens the artefact as the installer would and
|
||||
# reads the packaged config (igneum-app.json: packaging/mac/packaged-config.sh write_packaged_config) the app reads at start:
|
||||
# mac a .dmg (hdiutil on macOS) or the unpacked .app / its Contents dir: Contents/Resources/igneum-app.json
|
||||
# windows the payload .zip (packaging/windows/make-payload.sh) or its unpacked dir: igneum-app.json beside the engine;
|
||||
# a Setup .exe is opaque (Inno Setup compresses its files): the payload zip is read from IGNEUM_WIN_PAYLOAD or
|
||||
# the igneum-windows-app-*.zip beside the installer, and the entry is refused when neither exists
|
||||
# hive the igneum-hive-*.tar.gz: no in-app updater (HiveOS installs the package its flight sheet names), so the check is
|
||||
# that igneum/version.txt names the node pin and no packaged config says the update check is off
|
||||
# RED when igneum-app.json is missing, unparsable, carries no update_manifest, one off the pattern
|
||||
# https://dl.igneum.network/dl/<token>/igneum-app-latest.json, or a token folder that is not one of the publisher's known
|
||||
# tokens (~/.config/igneum/dl-token, dl-token.next; IGNEUM_DL_TOKENS=<file,...> overrides; no token file at all: the pattern alone,
|
||||
# said so), or when the packager's "update check is disabled" line is found in the artefact. Tokens are never printed (masked).
|
||||
# tools/ci/packed-update-check.sh <artefact> [...] exit 0 every artefact reads an update path, 1 red (named), 2 bad args
|
||||
# tools/ci/packed-update-check.sh --self-test fake zip, dir and tarball artefacts prove the rule both ways
|
||||
set -euo pipefail
|
||||
PAT='^https://dl\.igneum\.network/dl/[a-z0-9]{6,}/igneum-app-latest\.json$'
|
||||
CFG="${IGNEUM_CONFIG_DIR:-$HOME/.config/igneum}"
|
||||
tokens() { # the publisher's known tokens, one per line (empty when no file exists)
|
||||
if [ -n "${IGNEUM_DL_TOKENS:-}" ]; then IFS=, read -ra fs <<<"$IGNEUM_DL_TOKENS"; for f in "${fs[@]}"; do [ -f "$f" ] && tr -d '[:space:]' < "$f" && echo; done; return 0; fi
|
||||
for f in "$CFG/dl-token" "$CFG/dl-token.next"; do [ -f "$f" ] && tr -d '[:space:]' < "$f" && echo; done; return 0
|
||||
}
|
||||
mask() { local s="$1" t; while read -r t; do [ -n "$t" ] && s="${s//$t/<token>}"; done < <(tokens); printf '%s' "$s"; }
|
||||
check_config() { # <json path> <label> -> 0 ok (line printed), 1 red
|
||||
local j="$1" what="$2" url tok known=0 n=0 t
|
||||
[ -f "$j" ] || { echo "RED $what: no igneum-app.json (the packaged config the app reads its update manifest from)"; return 1; }
|
||||
if grep -q "update check is disabled" "$j"; then echo "RED $what: the packaged config carries the packager's 'update check is disabled' line"; return 1; fi
|
||||
url="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("update_manifest") or "")' "$j" 2>/dev/null)" || { echo "RED $what: igneum-app.json does not parse"; return 1; }
|
||||
[ -n "$url" ] || { echo "RED $what: igneum-app.json carries no update_manifest (the build had no download token: the app would never update)"; return 1; }
|
||||
printf '%s' "$url" | grep -Eq "$PAT" || { echo "RED $what: update_manifest is not a token-folder manifest URL: $(mask "$url")"; return 1; }
|
||||
tok="${url#https://dl.igneum.network/dl/}"; tok="${tok%%/*}"
|
||||
while read -r t; do [ -n "$t" ] || continue; n=$((n+1)); [ "$t" = "$tok" ] && known=1; done < <(tokens)
|
||||
if [ "$n" -gt 0 ] && [ "$known" = 0 ]; then echo "RED $what: update_manifest names a token folder that is none of the publisher's $n known tokens (a stale or foreign token: the app would check a folder nobody publishes)"; return 1; fi
|
||||
echo "ok $what: update_manifest $(mask "$url")$( [ "$n" = 0 ] && echo ' (no token file here: the pattern alone was checked)')"
|
||||
}
|
||||
check_dir() { # <dir> <label>: an unpacked .app, a Contents dir, or a payload dir
|
||||
local d="$1" what="$2" j
|
||||
for j in "$d/Contents/Resources/igneum-app.json" "$d/Resources/igneum-app.json" "$d/igneum-app.json"; do [ -f "$j" ] && { check_config "$j" "$what"; return $?; }; done
|
||||
j="$(find "$d" -maxdepth 4 -name igneum-app.json -type f 2>/dev/null | head -n1)"; check_config "${j:-$d/igneum-app.json}" "$what"
|
||||
}
|
||||
check_dmg() {
|
||||
local f="$1" m rc=0
|
||||
command -v hdiutil >/dev/null || { echo "RED $f: a DMG is read with hdiutil (macOS); run this check on the publishing Mac"; return 1; }
|
||||
m="$(hdiutil attach -nobrowse -readonly -noverify "$f" 2>/dev/null | grep -oE '/Volumes/.*' | head -n1)"; [ -n "$m" ] || { echo "RED $f: the DMG does not mount"; return 1; }
|
||||
check_dir "$m" "$(basename "$f")" || rc=1; hdiutil detach "$m" -quiet 2>/dev/null || true; return $rc
|
||||
}
|
||||
check_zip() {
|
||||
local f="$1" t rc=0; t="$(mktemp -d)"; python3 - "$f" "$t" <<'PY' || { echo "RED $f: the zip does not open"; rm -rf "$t"; return 1; }
|
||||
import sys, zipfile
|
||||
z = zipfile.ZipFile(sys.argv[1]); names = [n for n in z.namelist() if n.endswith("igneum-app.json")]
|
||||
if names: z.extract(names[0], sys.argv[2]); open(sys.argv[2] + "/igneum-app.json", "wb").write(open(sys.argv[2] + "/" + names[0], "rb").read())
|
||||
PY
|
||||
check_config "$t/igneum-app.json" "$(basename "$f")" || rc=1; rm -rf "$t"; return $rc
|
||||
}
|
||||
check_hive() {
|
||||
local f="$1" v
|
||||
if tar -tzf "$f" 2>/dev/null | grep -q 'igneum-app.json$'; then local t; t="$(mktemp -d)"; tar -xzf "$f" -C "$t" --wildcards '*igneum-app.json' 2>/dev/null || tar -xzf "$f" -C "$t" 2>/dev/null; check_dir "$t" "$(basename "$f")"; local rc=$?; rm -rf "$t"; return $rc; fi
|
||||
v="$(tar -xzOf "$f" igneum/version.txt 2>/dev/null | head -n1 || true)"
|
||||
[ -n "$v" ] || { echo "RED $f: the HiveOS package carries no igneum/version.txt (the node pin the flight sheet installs)"; return 1; }
|
||||
if tar -xzOf "$f" igneum/h-run.sh igneum/h-config.sh 2>/dev/null | grep -q "update check is disabled"; then echo "RED $f: the package's scripts carry the 'update check is disabled' line"; return 1; fi
|
||||
echo "ok $(basename "$f"): no in-app updater (HiveOS installs the package its flight sheet names); version.txt: $v"
|
||||
}
|
||||
check_exe() {
|
||||
local f="$1" p="${IGNEUM_WIN_PAYLOAD:-}"
|
||||
[ -n "$p" ] || p="$(ls -1 "$(dirname "$f")"/igneum-windows-app-*.zip 2>/dev/null | head -n1 || true)"
|
||||
[ -n "$p" ] && [ -f "$p" ] || { echo "RED $(basename "$f"): a Setup exe is opaque; the Windows payload zip (igneum-windows-app-*.zip, IGNEUM_WIN_PAYLOAD or beside the installer) is what carries igneum-app.json, and none was found"; return 1; }
|
||||
check_zip "$p" | sed "s#^\(ok\|RED\) #\1 $(basename "$f") via #"; return "${PIPESTATUS[0]}"
|
||||
}
|
||||
check_one() {
|
||||
local f="$1"
|
||||
if [ -d "$f" ]; then check_dir "$f" "$(basename "$f")"; return $?; fi
|
||||
[ -f "$f" ] || { echo "RED $f: no such artefact"; return 1; }
|
||||
case "$f" in
|
||||
*.dmg) check_dmg "$f" ;; *.zip) check_zip "$f" ;; *.tar.gz|*.tgz) check_hive "$f" ;; *.exe) check_exe "$f" ;;
|
||||
*) echo "RED $f: not a DMG, payload zip, Setup exe, HiveOS tarball or unpacked dir"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
self_test() {
|
||||
local T; T="$(mktemp -d)"; local fails=0 out
|
||||
mkdir -p "$T/cfg" "$T/good/Contents/Resources" "$T/none/Contents/Resources" "$T/empty" "$T/foreign" "$T/off"
|
||||
echo tokabc123 > "$T/cfg/dl-token"; echo toknext456 > "$T/cfg/dl-token.next"; export IGNEUM_CONFIG_DIR="$T/cfg"; unset IGNEUM_DL_TOKENS
|
||||
printf '{"update_manifest":"https://dl.igneum.network/dl/tokabc123/igneum-app-latest.json","channel":"x"}\n' > "$T/good/Contents/Resources/igneum-app.json"
|
||||
printf '{"channel":"x"}\n' > "$T/none/Contents/Resources/igneum-app.json"
|
||||
printf '{"update_manifest":"","channel":"x"}\n' > "$T/empty/igneum-app.json"
|
||||
printf '{"update_manifest":"https://dl.igneum.network/dl/tokstale9/igneum-app-latest.json"}\n' > "$T/foreign/igneum-app.json"
|
||||
printf '{"update_manifest":"https://dl.igneum.network/dl/tokabc123/igneum-app-latest.json","note":"the update check is disabled in this build"}\n' > "$T/off/igneum-app.json"
|
||||
t() { local want="$1" name="$2"; shift 2; out="$("$@" 2>&1)" && rc=0 || rc=$?; if [ "$rc" != "$want" ]; then echo "self-test failed: $name: exit $rc, wanted $want: $out"; fails=1; fi; }
|
||||
t 0 "a packaged config with the manifest URL in a known token folder passes" bash "$0" "$T/good"
|
||||
t 1 "no update_manifest is red" bash "$0" "$T/none"
|
||||
t 1 "an empty update_manifest is red" bash "$0" "$T/empty"
|
||||
t 1 "a token folder outside the known tokens is red" bash "$0" "$T/foreign"
|
||||
t 1 "the packager's disabled line is red" bash "$0" "$T/off"
|
||||
(cd "$T/good" && python3 -c 'import zipfile; z=zipfile.ZipFile("../payload.zip","w"); z.write("Contents/Resources/igneum-app.json","igneum-windows-app/igneum-app.json"); z.close()')
|
||||
t 0 "a Windows payload zip with the manifest URL passes" bash "$0" "$T/payload.zip"
|
||||
(cd "$T/none" && python3 -c 'import zipfile; z=zipfile.ZipFile("../payload-none.zip","w"); z.write("Contents/Resources/igneum-app.json","igneum-windows-app/igneum-app.json"); z.close()')
|
||||
t 1 "a Windows payload zip without the manifest URL is red" bash "$0" "$T/payload-none.zip"
|
||||
: > "$T/Setup.exe"; t 1 "a Setup exe with no payload zip beside it is red" bash "$0" "$T/Setup.exe"
|
||||
cp "$T/payload.zip" "$T/igneum-windows-app-9.9.9-abcdef01.zip"; t 0 "a Setup exe with the payload zip beside it reads the zip" bash "$0" "$T/Setup.exe"
|
||||
mkdir -p "$T/hive/igneum"; echo "igneumd 2.0.3-27f54124" > "$T/hive/igneum/version.txt"; echo 'echo run' > "$T/hive/igneum/h-run.sh"; (cd "$T/hive" && COPYFILE_DISABLE=1 tar -czf ../igneum-hive-9.9.9.tar.gz igneum)
|
||||
t 0 "a HiveOS tarball with version.txt passes (no in-app updater)" bash "$0" "$T/igneum-hive-9.9.9.tar.gz"
|
||||
mkdir -p "$T/hive2/igneum"; echo 'echo run' > "$T/hive2/igneum/h-run.sh"; (cd "$T/hive2" && COPYFILE_DISABLE=1 tar -czf ../igneum-hive-0.0.0.tar.gz igneum)
|
||||
t 1 "a HiveOS tarball without version.txt is red" bash "$0" "$T/igneum-hive-0.0.0.tar.gz"
|
||||
out="$(bash "$0" "$T/good" 2>&1)"; case "$out" in *tokabc123*) echo "self-test failed: the token was printed"; fails=1;; esac
|
||||
rm -rf "$T"; [ "$fails" = 0 ] && echo "self-test passed: an artefact publishes only when its packaged config names its update manifest in a known token folder; a missing, empty, foreign or disabled update path is red; the token is never printed; Windows reads the payload zip; HiveOS reads version.txt"
|
||||
return $fails
|
||||
}
|
||||
[ $# -ge 1 ] || { sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 2; }
|
||||
if [ "$1" = --self-test ]; then self_test; exit $?; fi
|
||||
rc=0; for a in "$@"; do check_one "$a" || rc=1; done; exit $rc
|
||||
|
|
@ -177,6 +177,7 @@ tree_checks() {
|
|||
run "the fleet prover's task protection (V6-08): the backpressure gate, task sizing, shard ordering, preflight verdicts and the flow identity on known-failed-first cases" python3 tools/fleet/box-prover.py --self-test
|
||||
run "the registry's evidence rules: a PASS names evidence that exists, a touched evidence file moves with its row, stale evidence never reads PASS, a run_status needs the approval (self-test)" bash tools/ci/registry-evidence-check.sh --self-test
|
||||
run "the kit ISA check: a distribution kit's binaries carry no AVX-512 encoding (the x86-64-v3 baseline; self-test with a fake objdump)" bash tools/ci/kit-isa-check.sh --self-test
|
||||
run "rule 38: an entry's artefact carries its update manifest URL in a known token folder; a build whose update check is off never publishes (self-test)" bash tools/ci/packed-update-check.sh --self-test
|
||||
run "F02 (Review B): the proof-rule test bypass cannot reach a release build: an env read with no cfg guard or under a default feature is red; a release binary carrying the bypass string is red (self-test)" bash tools/ci/proof-rule-bypass-check.sh --self-test
|
||||
run "the test map merges structurally at a landing: master's cells plus the branch's, minus what the branch removed and master left (self-test)" python3 tools/ci/test-map-merge.py --self-test
|
||||
run "the box-form hashed identity list hashes a path entry's login only, never a generic path word (self-test)" bash tools/ci/served-identity-hashes.sh --self-test
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ if [ "${1:-}" = --self-test ]; then
|
|||
grep -q 'leak.json line 1 <redacted>' "$d/refusals.log" 2>/dev/null || { echo "self-test failed: the refusal was not logged redacted"; fails=1; }
|
||||
grep -q 'OwnerName' "$d/refusals.log" 2>/dev/null && { echo "self-test failed: the refusals log carries the matched text"; fails=1; }
|
||||
IGNEUM_IDENTITY_LOCAL="$d/identity.local" bash "$ME" "$d/rig.json" >/dev/null 2>&1 && { echo "self-test failed: a served file carrying a rig name passed"; fails=1; }
|
||||
rc=0; IGNEUM_IDENTITY_LOCAL="$d/missing.local" HOME="$d" bash "$ME" "$d/clean.json" >/dev/null 2>&1 || rc=$?; [ "$rc" = 2 ] || { echo "self-test failed: a publish host without the private list was not refused with exit 2 (got $rc)"; fails=1; }
|
||||
IGNEUM_IDENTITY_LOCAL="$d/missing.local" HOME="$d" IGNEUM_IDENTITY_LOCAL_OPTIONAL=1 bash "$ME" "$d/clean.json" >/dev/null 2>&1 || { echo "self-test failed: a tree check without the private list was refused"; fails=1; }
|
||||
rc=0; IGNEUM_IDENTITY_LOCAL="$d/missing.local" HOME="$d" IGNEUM_IDENTITY_HASHES="$d/missing.hashes" IGNEUM_IDENTITY_SALT="$d/missing.salt" bash "$ME" "$d/clean.json" >/dev/null 2>&1 || rc=$?; [ "$rc" = 2 ] || { echo "self-test failed: a publish host without the private list was not refused with exit 2 (got $rc)"; fails=1; }
|
||||
IGNEUM_IDENTITY_LOCAL="$d/missing.local" HOME="$d" IGNEUM_IDENTITY_HASHES="$d/missing.hashes" IGNEUM_IDENTITY_SALT="$d/missing.salt" IGNEUM_IDENTITY_LOCAL_OPTIONAL=1 bash "$ME" "$d/clean.json" >/dev/null 2>&1 || { echo "self-test failed: a tree check without the private list was refused"; fails=1; }
|
||||
# the box form: a hashed token list and a salt, no clear list; the owner's name token and the home IP are refused, a clean file passes
|
||||
printf 'saltsaltsaltsaltsaltsaltsaltsalt' > "$d/salt"; python3 -c "
|
||||
import hmac,hashlib; salt=open('$d/salt','rb').read()
|
||||
|
|
|
|||
|
|
@ -36,8 +36,13 @@ def build():
|
|||
disk = jl("disk.json", {})
|
||||
boxes = []; vast = 0.0; runpod = 0.0
|
||||
for iid, b in reg.items():
|
||||
h = b.get("hours") if b.get("state") == "destroyed" else hours(b["rented_at"])
|
||||
cost = round(h * b["dph"], 3)
|
||||
# a registry row without a label, card or rent time (a rent that never answered, a destroyed row written by the
|
||||
# destroy path alone) is not a box the page can describe: skipped, never a stop (9 October 2026)
|
||||
if not b.get("label") or not b.get("card") or not b.get("rented_at"): continue
|
||||
# a row with no hours, rent time or price (a rent that never answered, a registry row written by hand) costs nothing
|
||||
# rather than stopping the page (9 October 2026: one such row stopped every publish since 7 October)
|
||||
h = (b.get("hours") or 0) if b.get("state") == "destroyed" else (hours(b["rented_at"]) if b.get("rented_at") else 0)
|
||||
cost = round((h or 0) * (b.get("dph") or 0), 3)
|
||||
if b.get("provider", "vast") == "vast": vast += cost
|
||||
else: runpod += cost
|
||||
boxes.append({"id": iid, "label": b["label"], "card": b["card"], "vram_gb": round((b.get("vram_mb") or 0) / 1024), "provider": b.get("provider", "vast"),
|
||||
|
|
|
|||
|
|
@ -163,6 +163,13 @@
|
|||
.server > .row .rstat { flex: 1 1 auto; opacity: .9; }
|
||||
.server > .row .rage { opacity: .7; font-size: 12px; }
|
||||
.server:not(.open) > .panel { display: none; }
|
||||
.crew .w[data-box] { cursor: pointer; }
|
||||
.crew .w[data-box]:hover { border-color: var(--ember-2); }
|
||||
.crew .w[data-box] .card::before { content: '\25B8'; display: inline-block; width: 14px; opacity: .8; }
|
||||
.crew .w[data-box].open .card::before { content: '\25BE'; }
|
||||
.crew .w[data-box].open { border-color: var(--ember-2); }
|
||||
.crew > .server { grid-column: 1 / -1; margin-top: 0; }
|
||||
.crew > .server > .row { display: none; }
|
||||
</style></head><body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
|
|
@ -170,13 +177,11 @@
|
|||
<svg viewBox="0 0 100 100" aria-hidden="true"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"/><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"/></svg>
|
||||
<h1>Igneum Workers<small>build shop</small></h1>
|
||||
</div>
|
||||
<nav aria-label="Pages"><a href="./">Fleet</a><a href="workers.html" class="on" aria-current="page">Workers</a></nav>
|
||||
<nav aria-label="Pages"><a href="https://dl.igneum.network/fleet-22adafa34bc2/">Fleet</a><a href="workers.html" class="on" aria-current="page">Workers</a></nav>
|
||||
<div class="live" id="live"><b></b><span id="stamp">waiting for the first write</span></div>
|
||||
</header>
|
||||
|
||||
<div id="servers"></div>
|
||||
|
||||
<h2>Workers <span>every machine that builds, tests or measures</span></h2>
|
||||
<h2>Workers <span>every machine that builds, tests or measures; click a box for its cores, load and jobs</span></h2>
|
||||
<div class="crew" id="crew"></div>
|
||||
|
||||
<h2>Now building <span id="now-sub"></span></h2>
|
||||
|
|
@ -278,14 +283,14 @@ function serverSection(b, i) {
|
|||
<div class="g">${arc(b.slots ? (buildHeld(b) / Math.max(1, b.slots.count)) * 100 : 0, 'good')}<div><div class="l">Build slots</div><div class="v">${b.slots ? `${buildHeld(b)}/${b.slots.count}` : '–'}</div><div class="s">${b.queue && b.queue.length ? `${b.queue.length} waiting${b.queue.some(q => q.priority === 'gate') ? ', a gate first' : ''}` : 'nobody waiting'}</div></div></div>
|
||||
</div></div></section>`;
|
||||
}
|
||||
// the per-box detail panels are collapsed by default (the founder, 9 October 2026 09:5x UK): one compact row per box, the panel
|
||||
// opens on click, several at once, the open set kept per viewer in localStorage (nothing leaves the browser)
|
||||
// the per-box detail panels are collapsed by default (the founder, 9 October 2026 09:5x UK) and open under the box's card on
|
||||
// click, several at once, the open set kept per viewer in localStorage (nothing leaves the browser)
|
||||
const OPEN_KEY = 'workers.open-boxes';
|
||||
function openSet() { try { return new Set(JSON.parse(localStorage.getItem(OPEN_KEY) || '[]')); } catch { return new Set(); } }
|
||||
function isOpen(name) { return openSet().has(name); }
|
||||
function toggleBox(name) { const o = openSet(); if (o.has(name)) o.delete(name); else o.add(name); try { localStorage.setItem(OPEN_KEY, JSON.stringify([...o])); } catch {} renderServers(); }
|
||||
document.addEventListener('click', e => { const row = e.target.closest('.server > .row'); if (!row) return; toggleBox(row.parentElement.dataset.box); });
|
||||
document.addEventListener('keydown', e => { if (e.key !== 'Enter' && e.key !== ' ') return; const row = e.target.closest && e.target.closest('.server > .row'); if (!row) return; e.preventDefault(); toggleBox(row.parentElement.dataset.box); });
|
||||
function toggleBox(name) { const o = openSet(); if (o.has(name)) o.delete(name); else o.add(name); try { localStorage.setItem(OPEN_KEY, JSON.stringify([...o])); } catch {} renderCrew(); }
|
||||
document.addEventListener('click', e => { const w = e.target.closest('.crew > .w[data-box]'); if (!w) return; toggleBox(w.dataset.box); });
|
||||
document.addEventListener('keydown', e => { if (e.key !== 'Enter' && e.key !== ' ') return; const w = e.target.closest && e.target.closest('.crew > .w[data-box]'); if (!w) return; e.preventDefault(); toggleBox(w.dataset.box); });
|
||||
// the Mac mini (igneum-mini, the Mac build box, M6): its own collector pushes mini.json to build-1; until then its card reads no report yet
|
||||
const MINI = { name: 'igneum-mini', label: 'the Mac build box, M6' };
|
||||
const miniLive = () => { const m = D && D.mini; return m && m.source && m.source.ok && m.cores ? m : null; };
|
||||
|
|
@ -294,7 +299,9 @@ function miniSection() {
|
|||
if (!m) return `<section class="server" aria-label="${MINI.name}"><div class="head"><div><div class="name">${MINI.name}<small>${MINI.label}</small></div><div class="facts"><span>no report yet</span></div></div></div></section>`;
|
||||
return serverSection({ ...m, name: MINI.name, os: MINI.label, log: m.log || { present: true } }, 99);
|
||||
}
|
||||
function renderServers() { const bx = boxesOf(D); $('servers').innerHTML = (bx.length ? bx.map(serverSection).join('') : serverSection(null, 0)) + miniSection(); }
|
||||
// the detail (cores, load by process, gauges) opens under a box's card in the Workers grid (the founder, 9 October 2026 11:5x UK:
|
||||
// the compact row list above the cards is gone; the cards themselves expand)
|
||||
function boxDetail(b, i) { return isOpen(String(b.name || '')) ? serverSection(b, i) : ''; }
|
||||
|
||||
function slotBar(slots, count) {
|
||||
const held = new Map((slots && slots.held || []).map(h => [h.slot, h]));
|
||||
|
|
@ -309,7 +316,7 @@ function renderCrew() {
|
|||
const st = boxState(bx);
|
||||
if (st.kind === 'down') { cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">build server</div></div>${pill('down', 'error')}</div><div class="doing">${esc(st.note)}</div></div>`); continue; }
|
||||
if (st.kind === 'provisioning' && !st.live) { cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx && bx.name || 'build server')}</div><div class="meta">build server</div></div>${pill('provisioning', 'queued')}</div><div class="doing">${esc(st.note)}</div></div>`); continue; }
|
||||
if (bx && bx.cores) cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">dedicated box, ${bx.cores} threads, ${esc(fmtBytes((bx.mem && bx.mem.total_kb || 0) * 1024))}, RAID 1 NVMe</div></div>${pill(bx.running && bx.running.length ? 'building' : jobsSummary(bx) ? 'busy' : 'idle', bx.running && bx.running.length ? 'running' : '')}</div><div class="doing">${bx.running && bx.running.length ? bx.running.map(r => esc(`${r.worktree || '?'}: ${kindLabel(r.kind)}`)).join('<br>') : (st.kind === 'provisioning' ? esc(st.note) : 'Slot free. The next build-remote.sh or cross-remote.sh routed here takes it.')}</div>${slotBar(bx.slots, bx.slots ? bx.slots.count : 1)}<div class="foot"><span>${bx.recent ? bx.recent.length : 0} builds logged</span><span>${esc(ago(bx.collected_at))}</span></div></div>`);
|
||||
if (bx && bx.cores) { const k = String(bx.name || ''); cards.push(`<div class="w ${isOpen(k) ? 'open' : ''}" data-box="${esc(k)}" role="button" tabindex="0" aria-expanded="${isOpen(k) ? 'true' : 'false'}" title="click to ${isOpen(k) ? 'close' : 'open'} the detail"><div class="top"><div><div class="card">${esc(bx.name)}</div><div class="meta">dedicated box, ${bx.cores} threads, ${esc(fmtBytes((bx.mem && bx.mem.total_kb || 0) * 1024))}, RAID 1 NVMe</div></div>${pill(bx.running && bx.running.length ? 'building' : jobsSummary(bx) ? 'busy' : 'idle', bx.running && bx.running.length ? 'running' : '')}</div><div class="doing">${bx.running && bx.running.length ? bx.running.map(r => esc(`${r.worktree || '?'}: ${kindLabel(r.kind)}`)).join('<br>') : (st.kind === 'provisioning' ? esc(st.note) : 'Slot free. The next build-remote.sh or cross-remote.sh routed here takes it.')}</div>${slotBar(bx.slots, bx.slots ? bx.slots.count : 1)}<div class="foot"><span>${bx.recent ? bx.recent.length : 0} builds logged</span><span>${esc(ago(bx.collected_at))}</span></div></div>`); cards.push(boxDetail(bx, boxesOf(D).indexOf(bx))); }
|
||||
else cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(bx && bx.name || 'build server')}</div><div class="meta">build server</div></div>${pill('unreachable', 'error')}</div><div class="doing">${esc(bx && bx.source && bx.source.error || 'no facts from this box')}</div></div>`);
|
||||
}
|
||||
if (mac) {
|
||||
|
|
@ -317,8 +324,8 @@ function renderCrew() {
|
|||
const doing = held.length ? held.map(h => `${esc(h.slot)}: ${esc((h.worktree ? h.worktree + ' ' : '') + (h.command || h.label || ''))}`.slice(0, 140)).join('<br>') : MAC_COPY;
|
||||
cards.push(`<div class="w"><div class="top"><div><div class="card">${esc(mac.name)}</div><div class="meta">this Mac, ${mac.build_slots} build slot${mac.build_slots === 1 ? '' : 's'}, 3 run slots</div></div>${pill(meas.length ? 'measuring' : builds.length ? 'building' : runs.length ? 'running' : 'idle', held.length ? 'running' : '')}</div><div class="doing">${doing}</div><div class="slots">${['build-0', 'build-1', 'build-2', 'run-0', 'run-1', 'run-2', 'measure'].map(nm => { const h = held.find(x => x.slot === nm); return `<i class="${h ? (nm === 'measure' ? 'measure' : nm.startsWith('run') ? 'run' : 'held') : ''}" title="${esc(nm)}${h ? ': ' + esc(h.label) : ': free'}"></i>`; }).join('')}</div><div class="foot"><span>${mac.queue.length ? mac.queue.length + ' waiting for a slot' : 'nobody waiting'}</span><span>${esc(ago(mac.collected_at))}</span></div></div>`);
|
||||
} else cards.push(`<div class="w"><div class="top"><div><div class="card">the macOS build host</div><div class="meta">this Mac</div></div>${pill('no push', 'error')}</div><div class="doing">${MAC_COPY}</div></div>`);
|
||||
{ const m = miniLive(); const held = m && m.slots && m.slots.held || [];
|
||||
cards.push(`<div class="w"><div class="top"><div><div class="card">${MINI.name}</div><div class="meta">${MINI.label}</div></div>${pill(!m ? 'no report' : held.length ? 'building' : 'idle', !m ? 'queued' : held.length ? 'running' : '')}</div><div class="doing">${!m ? 'no report yet' : (esc(miniMinerLine(m)) + '<br>') + (held.length ? held.map(h => esc(`${h.slot}: ${h.worktree || h.label || ''}`)).join('<br>') : 'macOS binaries build here from tonight; slot free.')}</div>${m && m.slots ? slotBar(m.slots, m.slots.count || 1) : ''}<div class="foot"><span>${m ? `${(m.recent || []).length} builds logged` : 'its collector pushes to build-1'}</span><span>${m ? esc(ago(m.collected_at)) : ''}</span></div></div>`); }
|
||||
{ const m = miniLive(); const held = m && m.slots && m.slots.held || []; const mk = MINI.name, mo = m && isOpen(mk);
|
||||
cards.push(`<div class="w ${mo ? 'open' : ''}" ${m ? `data-box="${mk}" role="button" tabindex="0" aria-expanded="${mo ? 'true' : 'false'}" title="click to ${mo ? 'close' : 'open'} the detail"` : ''}><div class="top"><div><div class="card">${MINI.name}</div><div class="meta">${MINI.label}</div></div>${pill(!m ? 'no report' : held.length ? 'building' : 'idle', !m ? 'queued' : held.length ? 'running' : '')}</div><div class="doing">${!m ? 'no report yet' : (esc(miniMinerLine(m)) + '<br>') + (held.length ? held.map(h => esc(`${h.slot}: ${h.worktree || h.label || ''}`)).join('<br>') : 'macOS binaries build here from tonight; slot free.')}</div>${m && m.slots ? slotBar(m.slots, m.slots.count || 1) : ''}<div class="foot"><span>${m ? `${(m.recent || []).length} builds logged` : 'its collector pushes to build-1'}</span><span>${m ? esc(ago(m.collected_at)) : ''}</span></div></div>`); if (mo) cards.push(serverSection({ ...m, name: MINI.name, os: MINI.label, log: m.log || { present: true } }, 99)); }
|
||||
const pcsAt = D.pcs && D.pcs.collected_at ? Date.parse(D.pcs.collected_at) : null; const pcsAge = pcsAt ? Math.round((Date.now() - pcsAt) / 1000) : null;
|
||||
const pcsAgeText = pcsAge === null ? 'report age unknown' : `report ${fmtDurShort(pcsAge)} old${pcsAge > 600 ? ', STALE' : ''}`;
|
||||
// a PC's five-minute report through the intake (9 October 2026): the known fields on one line, the rest of the report left in workers.json
|
||||
|
|
@ -467,8 +474,7 @@ function apply(d) {
|
|||
const live = $('live'); live.className = 'live' + (age > 900 ? ' down' : age > 420 ? ' stale' : '');
|
||||
const u = ukTime(d.generated_at);
|
||||
$('stamp').innerHTML = `${age > 420 ? 'stale: ' : ''}written ${t(d.generated_at)} (${esc(ago(d.generated_at))})${d._feed ? ' · ' + esc(d._feed) : ''}`;
|
||||
const el = $('servers'); if (el && !el.innerHTML) el.innerHTML = '';
|
||||
renderServers(); renderCrew(); renderNow(); renderQueue(); renderBackground(); renderDone(); renderHeadline(); renderAnalytics(); renderSources();
|
||||
renderCrew(); renderNow(); renderQueue(); renderBackground(); renderDone(); renderHeadline(); renderAnalytics(); renderSources();
|
||||
}
|
||||
let feed = '';
|
||||
async function fetchJson(url, ms) { const r = await fetch(`${url}?t=${Date.now()}`, { cache: 'no-store', signal: AbortSignal.timeout(ms) }); if (!r.ok) throw new Error(`http ${r.status}`); return r.json(); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue