Skip to content

Instantly share code, notes, and snippets.

@sethwebster
Last active January 29, 2026 13:32
Show Gist options
  • Select an option

  • Save sethwebster/1c9525a8378cbc2f7a90fd122b1f21a1 to your computer and use it in GitHub Desktop.

Select an option

Save sethwebster/1c9525a8378cbc2f7a90fd122b1f21a1 to your computer and use it in GitHub Desktop.
(async () => {
// =========================
// CONFIG
// =========================
const CFG = {
// Collection
sidebarSelector: 'nav[aria-label="Chat history"]',
chatLinkSelector: 'a[href^="/c/"]',
scrollStepRatio: 0.85, // scroll by 85% of viewport height per step
scrollDelayMs: 300,
stableRoundsToStop: 8, // stop after N scrolls with no new IDs
// Archiving
concurrency: 3, // keep low to avoid rate limiting
perRequestDelayMs: 80, // tiny pacing between launches
retryCount: 3,
retryBackoffMs: 400,
dryRun: false, // set true to only collect IDs
debug: true,
};
const log = (...a) => CFG.debug && console.log("[bulk-archive]", ...a);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// =========================
// TOKEN (read from client-bootstrap)
// =========================
const bootstrapEl = document.getElementById("client-bootstrap");
if (!bootstrapEl) throw new Error("Missing #client-bootstrap (cannot read session access token).");
let accessToken;
try {
const bootstrap = JSON.parse(bootstrapEl.textContent);
accessToken = bootstrap?.session?.accessToken;
} catch (e) {
throw new Error("Failed to parse #client-bootstrap JSON.");
}
if (!accessToken) throw new Error("No session.accessToken found in #client-bootstrap.");
// =========================
// SIDEBAR + ID COLLECTION
// =========================
const sidebar = document.querySelector(CFG.sidebarSelector);
if (!sidebar) throw new Error(`Sidebar not found: ${CFG.sidebarSelector}`);
const idFromHref = (href) => {
// href can be "/c/<id>" or full URL.
const m = String(href).match(/\/c\/([0-9a-f-]{20,})/i);
return m ? m[1] : null;
};
const collectVisibleIds = () => {
const links = Array.from(sidebar.querySelectorAll(CFG.chatLinkSelector));
const ids = [];
for (const a of links) {
const id = idFromHref(a.getAttribute("href") || a.href);
if (id) ids.push(id);
}
return ids;
};
const allIds = new Set();
let stableRounds = 0;
log("Collecting chat IDs by scrolling… (make sure you are filtered to outside projects)");
while (stableRounds < CFG.stableRoundsToStop) {
const before = allIds.size;
for (const id of collectVisibleIds()) allIds.add(id);
const after = allIds.size;
if (after === before) stableRounds++;
else stableRounds = 0;
// Scroll down
const prevTop = sidebar.scrollTop;
sidebar.scrollTop = prevTop + Math.floor(sidebar.clientHeight * CFG.scrollStepRatio);
await sleep(CFG.scrollDelayMs);
// If scrolling no longer moves and we're stable, we’ll stop naturally
}
const ids = Array.from(allIds);
log(`Collected ${ids.length} chat IDs.`);
if (CFG.dryRun) {
console.log(ids);
log("Dry run enabled; not archiving.");
return;
}
// =========================
// ARCHIVE FUNCTION
// =========================
const archiveOne = async (id) => {
const url = `https://chatgpt.com/backend-api/conversation/${id}`;
const body = JSON.stringify({ is_archived: true });
for (let attempt = 0; attempt <= CFG.retryCount; attempt++) {
try {
const res = await fetch(url, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Accept": "*/*",
"Authorization": `Bearer ${accessToken}`,
},
body,
credentials: "include",
});
if (res.ok) return { id, ok: true, status: res.status };
// Retry on rate limits / transient errors
const retryable = res.status === 429 || (res.status >= 500 && res.status <= 599);
if (!retryable || attempt === CFG.retryCount) {
const text = await res.text().catch(() => "");
return { id, ok: false, status: res.status, error: text.slice(0, 200) };
}
await sleep(CFG.retryBackoffMs * (attempt + 1));
} catch (e) {
if (attempt === CFG.retryCount) {
return { id, ok: false, status: 0, error: String(e) };
}
await sleep(CFG.retryBackoffMs * (attempt + 1));
}
}
return { id, ok: false, status: 0, error: "unknown" };
};
// =========================
// WORK QUEUE WITH LIMITED CONCURRENCY
// =========================
let idx = 0;
const results = [];
const workers = Array.from({ length: CFG.concurrency }, (_, w) => (async () => {
while (idx < ids.length) {
const myIdx = idx++;
const id = ids[myIdx];
const r = await archiveOne(id);
results.push(r);
if (r.ok) log(`✅ ${myIdx + 1}/${ids.length} archived ${id}`);
else log(`❌ ${myIdx + 1}/${ids.length} failed ${id} (status ${r.status})`, r.error || "");
await sleep(CFG.perRequestDelayMs);
}
})());
await Promise.all(workers);
const ok = results.filter(r => r.ok).length;
const bad = results.length - ok;
log(`Done. Success: ${ok}, Failed: ${bad}`);
// Print failures for easy re-run
const failedIds = results.filter(r => !r.ok).map(r => r.id);
if (failedIds.length) {
console.warn("Failed IDs (copy to retry):", failedIds);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment