Retire the rotation queue's voice; team links pick the nearest open spot in their own leg

The queue's AUTO-ADVANCE post announced a rotation event this morning when a member had
built his own leg (the Triple Play). The queue stopped placing anyone when the public link
moved to the chain walk; what was left of it was three Telegram messages, a classifier that
called any join under a former queue member a "rotation join" and swept the newcomer into
the queue, and a preference inside the team-link picker for queue positions over the nearest
open one (15 members affected).

Verified offline over the full snapshot first: all 889 personal links already target the
member or a position inside their own subtree; the walk starts at the member's own children
and cannot leave the leg. Now: the three messages are gone (counters still update silently);
a join is a rotation join only if the company link could have produced it (the company link
always offers the shallowest open position, so a deeper referrer rules it out; 'invite-<id>'
submissions are leg joins outright); nothing is auto-added to the queue; and the team link
picks the nearest open position in the member's own leg, in tree order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-19 06:50:32 -05:00
parent 97ddea7e1e
commit 4981b1a923
2 changed files with 50 additions and 52 deletions
+17 -5
View File
@@ -762,14 +762,26 @@ function memberIdByAccount(address) {
function nextOpenPosition(rootId, exclude) {
if (!state || !state.snapshotAt) return null;
const skip = exclude || new Set();
const q = [rootId]; const seen = new Set();
const q = [[rootId, 0]]; const seen = new Set();
while (q.length) {
const id = q.shift(); if (seen.has(id)) continue; seen.add(id);
const [id, depth] = q.shift(); if (seen.has(id)) continue; seen.add(id);
const m = state.members[id]; if (!m) continue;
// excluded positions are never offered as the target, but their subtree
// is still traversed (founders can reserve a position's open slots)
if (!skip.has(id) && (m.directCount || 0) < 2) return { id, directCount: m.directCount || 0, level: levelName(m.level || 1) };
if (m.l) q.push(m.l); if (m.r) q.push(m.r);
if (!skip.has(id) && (m.directCount || 0) < 2) return { id, depth, directCount: m.directCount || 0, level: levelName(m.level || 1) };
if (m.l) q.push([m.l, depth + 1]); if (m.r) q.push([m.r, depth + 1]);
}
return null;
}
// How deep a position sits under a root (0 = the root itself); null when it is not under it.
function depthFrom(rootId, id) {
if (!state || !state.members) return null;
const q = [[rootId, 0]]; const seen = new Set();
while (q.length) {
const [cur, depth] = q.shift(); if (seen.has(cur)) continue; seen.add(cur);
if (Number(cur) === Number(id)) return depth;
const m = state.members[cur]; if (!m) continue;
if (m.l) q.push([m.l, depth + 1]); if (m.r) q.push([m.r, depth + 1]);
}
return null;
}
@@ -829,4 +841,4 @@ async function getIncome(id) {
};
}
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getCoachingScan, getMatrixTree, isInTeam, nextOpenPosition, memberIdByAccount, liveDirects, balanceOf, CONTRACT };
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getCoachingScan, getMatrixTree, isInTeam, nextOpenPosition, depthFrom, memberIdByAccount, liveDirects, balanceOf, CONTRACT };
+33 -47
View File
@@ -362,19 +362,33 @@ async function handleSubmitId(req, res) {
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000))
]);
} catch (e) { onchain = null; }
// the chain decides the path: a rotation join is one whose on-chain referrer is
// a rotation-queue sponsor that IS or HAS BEEN worked (status active or
// qualified) — not just the currently-active one. With auto-advance, the direct
// who completes a sponsor's 2/2 (and thus joined under it) submits their ID
// AFTER the rotation has already moved on, so keying off "active only" wrongly
// labeled them a leg join. A referrer that's a still-waiting queue position, or
// not in the queue at all, is a personal leg join.
const sponsorsNow = getSponsors();
const active = activeSponsor(sponsorsNow);
// Which link produced this join. In 'chain' mode the company link always offers the
// SHALLOWEST open position under the root, so a join whose on-chain referrer sits deeper
// than some other open position cannot have come from it: it came through somebody's
// personal link. A personal page also tags its submissions 'invite-<id>', which settles it
// outright. The old queue-membership test is kept only for the legacy 'queue' mode.
// (2026-09-19: #861's own join under Terry #840 was called a rotation join because Terry
// had once qualified through the queue, and #861 was swept into a queue that no longer
// places anyone.)
const cfgNow = getConfig();
let joinPath = 'unknown';
if (onchain && onchain.registered) {
const refSp = sponsorsNow.find(s => String(s.id) === String(onchain.referrerId));
joinPath = (refSp && (refSp.status === 'active' || refSp.status === 'qualified')) ? 'rotation' : 'leg';
const R = Number(onchain.referrerId);
if ((cfgNow.publicRotationMode || 'queue') === 'chain') {
if (/^invite-/.test(source)) joinPath = 'leg';
else {
const root = Number(cfgNow.publicRotationRootId) || 2;
const exclude = new Set(String(cfgNow.rotationExcludeIds || '').split(',').map(n => Number(n.trim())).filter(n => n > 0));
exclude.add(R);
const dR = chain.depthFrom(root, R);
const other = chain.nextOpenPosition(root, exclude);
joinPath = (dR != null && (!other || other.depth >= dR)) ? 'rotation' : 'leg';
}
} else {
const sponsorsNow = getSponsors();
const refSp = sponsorsNow.find(s => String(s.id) === String(R));
joinPath = (refSp && (refSp.status === 'active' || refSp.status === 'qualified')) ? 'rotation' : 'leg';
}
} else if (onchain && !onchain.registered) joinPath = 'notfound';
subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString(), path: joinPath,
onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId, uplineId: onchain.uplineId } : undefined });
@@ -383,22 +397,8 @@ async function handleSubmitId(req, res) {
firePostback(clickid, `purchase-${clickid}`, source);
let msg;
if (joinPath === 'rotation') {
// rotation joins go straight into the queue as waiting positions — no manual step
let queueNote = '';
try {
let sponsors = getSponsors();
if (sponsors.some(s => String(s.id) === String(newId))) {
queueNote = 'Already in the rotation queue.';
} else {
const maxOrder = sponsors.reduce((m, s) => Math.max(m, s.sortOrder || 0), 0);
sponsors.push({ id: String(newId), name: memberName, parentId: String(onchain.referrerId), directs: 0, level: onchain.levelName || 'Scintilla', status: sponsors.some(s => s.status === 'active') ? 'waiting' : 'active', sortOrder: maxOrder + 10, clicks: 0, notes: `auto-added: rotation join under #${onchain.referrerId} ${new Date().toISOString().slice(0, 10)}` });
sponsors = normalizeStatuses(sponsors);
saveSponsors(sponsors);
const waitingAhead = sponsors.filter(s => s.status === 'waiting' && (s.sortOrder || 0) < maxOrder + 10).length;
queueNote = `Auto-added to the rotation queue (${waitingAhead} waiting ahead of them).`;
}
} catch (e) { queueNote = `⚠ Auto-add to queue failed (${e.message}) — add manually.`; console.error('queue auto-add', e.message); }
msg = `🔔 RM Circle: ROTATION JOIN CONFIRMED ✅\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nJoined under rotation sponsor: #${onchain.referrerId}\nSource: ${source||'(direct)'}\n${queueNote}\n(Sponsor #${onchain.referrerId}'s direct count syncs from the chain automatically.)`;
// company-link join: announce it; the queue is retired and nothing is added to it
msg = `🔔 RM Circle: ROTATION JOIN CONFIRMED ✅\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nJoined under rotation sponsor: #${onchain.referrerId}\nSource: ${source||'(direct)'}\n(Sponsor #${onchain.referrerId}'s direct count syncs from the chain automatically.)`;
} else if (joinPath === 'leg') {
msg = `🌱 RM Circle: TEAM-BUILD JOIN (not rotation)\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nActual sponsor on-chain: #${onchain.referrerId}${sponsorId!=='?'&&String(onchain.referrerId)!==sponsorId?` (form said ${sponsorId})`:''}\nSource: ${source||'(direct)'}\n→ Leg growth under #${onchain.referrerId} — no rotation action needed. Add them to the rotation queue only if they want the team effort.`;
} else if (joinPath === 'notfound') {
@@ -691,27 +691,16 @@ async function handleApi(req,res,pathname){
try{
const r=await Promise.race([chain.memberPublic(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Blockchain lookup timed out — try again.')),20000))]);
if(r.registered)r.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(id)}`;
// align next-in-line with the human-curated rotation: prefer the ACTIVE
// rotation sponsor when they sit in this member's leg and need directs;
// else the first chain-order position that's a queue participant; else
// keep the chain's pure structural pick (covers legs outside the queue).
// Where a member's team link sends the next join: the nearest position in THEIR OWN
// leg that still needs its two, in tree order ("your downline, in order", the words
// the training video uses). The walk starts at the member's own children, so it can
// never leave the leg. The old rotation queue used to be consulted first here; it no
// longer places anyone, so it no longer steers this either (2026-09-19).
if(r.registered&&r.subtree){
try{
const sponsors=getSponsors();
const act=activeSponsor(sponsors);
const participants=new Set(sponsors.filter(s=>s.status!=='qualified').map(s=>String(s.id)));
const bfs=[];const q=[r.subtree.left,r.subtree.right].filter(Boolean);
while(q.length){const n=q.shift();bfs.push(n);if(n.left)q.push(n.left);if(n.right)q.push(n.right);}
let pick=null;
if(act)pick=bfs.find(n=>String(n.id)===String(act.id)&&(n.directCount||0)<2);
if(!pick)pick=bfs.find(n=>participants.has(String(n.id))&&(n.directCount||0)<2);
// Last resort WITHIN the leg: the nearest position that still needs its 2,
// curated list or not. Without this, a member whose directs are too new to be
// in the rotation list had no leg pick at all and fell through to the GLOBAL
// rotation — sending their people to a stranger's position while their own
// brand-new directs sat on 0/2. That is the opposite of team-first. (Terry #840,
// qualified the same day he joined, was routing to #148; 2026-09-18.)
if(!pick)pick=bfs.find(n=>(n.directCount||0)<2);
const pick=bfs.find(n=>(n.directCount||0)<2);
if(pick)r.nextInLine={id:pick.id,directCount:pick.directCount||0,levelName:pick.levelName};
}catch(e){}
}
@@ -2032,11 +2021,9 @@ chain.startIndexer(evt=>{
sponsors=normalizeStatuses(sponsors);
saveSponsors(sponsors);
const next=activeSponsor(sponsors);
sendTelegram(`✅ AUTO-ADVANCE: queue sponsor #${s.id}${s.name?` (${s.name})`:''} reached 2/2 and is now QUALIFIED.\nRotation moved to ${next?`#${next.id}${next.name?` (${next.name})`:''}`:'— nobody waiting (add the next position to the queue)'}.`);
}else{
sponsors[idx].directs=newDirects;
saveSponsors(sponsors);
sendTelegram(`🤖 AUTO-COUNT: #${evt.id} is a DIRECT for queue sponsor #${s.id}${s.name?` (${s.name})`:''} — now ${newDirects}/2.`);
}
}
}
@@ -2050,7 +2037,6 @@ chain.startIndexer(evt=>{
if(idx>=0&&LEVELS.includes(evt.levelName)&&sponsors[idx].level!==evt.levelName){
sponsors[idx].level=evt.levelName;
saveSponsors(sponsors);
sendTelegram(`🤖 AUTO-LEVEL: queue sponsor #${sponsors[idx].id}${sponsors[idx].name?` (${sponsors[idx].name})`:''} upgraded on-chain — queue level updated to ${evt.levelName}.`);
}
}
}catch(e){console.error('auto-level error',e.message)}