What this is
"Tibo reset" = the head of OpenAI Codex/ChatGPT Work Thibault Sottiaux(X: @thsottiaux)
the tradition of broad quota resets announced on X. The community nicknames him "Lord Tibo", there is a third-party tracking site Tibo Radar
(codex-reset.com) and a "pray for reset" subculture. Tibo's official announcements have no fixed schedule; but the platform will also, when the quota
configuration switches,directly reset accounts without posting a reset post. So "no Tibo post" only proves there was no official announcement, not that
there was no reset.
First distinguish which kind of reset the user is asking about:
| Type | Who triggers it | Where to look | Nature |
|---|---|---|---|
| Announced broad RESET | Tibo / OpenAI | Official X original post; tracking sites only serve as announcement indexes | No fixed schedule, often used for milestones or outage compensation |
| Silent platform reset | OpenAI backend or quota configuration release | Product usage status + first-hand measurements of multiple accounts in the same period + ruling out their respective normal cycles; official quota changes only serve as context | There may be no reset post; when no official scope statement is obtained, one can only say "observed on a large scale", not "everyone" |
| BANKED reset | Tibo tweets, or official on-site events | Announcement: official original post—there may also be no original X post (distributed by pressing a button on site at the 2026-09-29 DevDay, first recorded by the operator of a third-party site, then confirmed by readings from multiple accounts under the username; at this time there was no official announcement, and crediting is judged by the user's direct observation of their own account or readings before and after the same account_ref ; if neither is available, then unverified); crediting: compare each account's in-product backup reset inventory readings before and after |
A one-time quota pack "kept for you to use whenever",it is not automatically consumed after crediting, and is manually redeemed by the user on the usage page; official announcement ≠ everyone gets it (there have been batch delays). Trigger forms include milestone celebrations and outage compensation—the rollout delay compensation described in §1 belongs to the latter (accumulated by days without access, and one person may get multiple grants; the 2026-09 GPT-6 Astra compensation is this form) |
| Account-level weekly reset | The system follows that account's current usage window | "Next reset: …" in the ChatGPT product | The time differs for each person; using Full reset also changes the weekly reset date, so do not calculate it from the subscription start date |
First determine the host: an application-hosted read-only research task
Only when the upper-layer application explicitly provides the research question, the complete previous report, pending status, valid corrections, information cutoff,
individually authorized private sources, and a structured output contract, and explicitly states that this round only performs reading and analysis and that persistent handoff belongs to the application, follow this branch.
Even if the host opens network and temporary workspace writes for authorized private sources, this does not change this branch's local ledger boundary.
This branch takes precedence over the local ledger steps of the "entry triage"
and "monitoring round" below: do not use forecast_log.py summary / handoff to read ledgers under the user
directory that may belong to another research scope, and do not execute finding / record / review writes;
do not execute the default of a bare call query_usage、scan_rollouts or account login/recovery paths, unless the host explicitly includes the
corresponding account/usage source in this round's authorization list. When unauthorized or unreadable, arrival and account status are
unknown, and are not inferred as arrived from announcements.
Using the host-provided state.entries and the previous round's report, take over monitoring round step 1, and use the final structured output to take over
step 5: write back pending commitments, candidate sources, types, windows, actual coverage, and the next review condition to
state.entries, preserving the original post in the report, distinguishing new evidence from old facts, and not describing a missing local ledger as
"there were no clues in the past." Continue executing the intermediate steps of Tibo source discovery, contextual corroboration, and
global / banked / both / unknown different usage decisions; the public source list is still only a starting point.
Only private sessions explicitly listed by the host may be read via read-wechat-messages , and the account precheck and image/voice coverage required by that Skill must be completed first; when not listed, precheck fails, or a read-only environment cannot handle it, mark
uncovered. Any other private source (including local account status) likewise presupposes item-by-item host authorization.
The change summary in the host output is only research pending verification and does not impersonate delivered, read, or formal account operations.
Independent calls that do not satisfy all trigger conditions in this section are executed according to the ordinary triage and local ledger process below.
Entry triage
-
Each call first followslocal prediction feedbackto read existing records; after the query obtains relevant event evidence, backfill pending predictions. When read-only records are empty, do not create a file; when proposing a new prediction, save the window, basis, and this round's feedback. Calls that actually fetch external data first write a findings record, and record/review link to it with evidence_refs (for the schema and rules see the "findings: raw reading layer" section of that document).
-
The user already knows the result and asks "why was it missed / which link was not covered / how to avoid it next time" -> switch tohistorical evidence chain diagnosis: locate layer by layer according to the index available at the time, the full original post, reply / quote / parent coverage, parsing results, and findings; when there is no original record from that round, keep "which step was lost in acquisition / parsing / extraction / recording = unknown." Do not repeat queries for known results in place of diagnosis, and do not re-check a banked reset the user has clearly said does not need checking. Only when the user also explicitly requests current account status, or the current reading would change this diagnosis, run the account SOP in parallel; this triage does not block explicit real-time queries.
-
The user askscurrentwhy the official announcement has not yet arrived, why there is still no execution signal -> followmonitoring round to investigate fulfillment status and reasons; this is not attribution for the previous round's missed report, and one must not merely replay old findings.
-
Bare call (no specific question, just wants to know the current situation) -> combined execution: ledger review -> announcement line (Radar index + independent Tibo main-post timeline + bounded reply discovery, according to each path in §1) + incident line (§1) -> local landing status (§2 script) -> monitoring round clue decisionand ledger backfill. According to the output contract, first give the current reset status conclusion, then give the next-step timing judgment along the prediction path; when there is no supportable date window, give a judgment on whether waiting is worthwhile, and do not call
recordto fabricate a window just to fill out the format. Each actual fetch by the announcement line, incident line, and local scan first writes a findings record, and when backfilling use evidence_refs to link (seethe findings section of prediction feedback)。§2 After that, a real-time banked query must be run once more(scripts/query_usage.py. For the reading method and field table, see Account SOP) — the §2 rollout snapshot structurally has no spare reset field; if you do not run it, you cannot fully answer "what is the situation now," the most frequently asked dimension; just take the single banked number and also record theaccount_reffrom the output into findings (for recording rules, see the privacy contract in forecast-feedback.md; for the comparable range of banked counts, see the Account SOP); the rest of this item still only concerns reset status. ⚠️ scan and banked must not be&&chained: scan_rollouts exits 1 when there is no snapshot, and&&will directly short-circuit the banked query — on the surface it looks like the full set ran, but in reality one dimension is missing (observed 2026-09-19). Run the two separately, or when chaining, give scan add|| true。In loop scenarios, bare commands in the skill are blocked bysame-cmd-resend-guardblock(after an endpoint flake fails once, resent byte-for-byte, it is blocked) — add at the start of the commandcd <skill 绝对路径> &&changing the structure lets it pass (observed 2026-09-19). When a scheduled loop (e.g./loop) is triggered repeatedly, if the interval since the last check is very short (<10 minutes) and the previous round had no signal of a changed call, you may run only the announcement line to confirm there is no new official announcement, and skip the full set of incidents/banked/local scan; this is a cost-reduction tradeoff, and dimensions not checked are still marked uncovered. When the interval is normal, clues in the ledger reach review conditions, or a new signal appeared in the previous round, still run the full set. Reduce frequency by time period(orthogonal to the by-interval item; the two stack): during Tibo's sleep period (America/Los_Angeles01:00–08:00; convert to Beijing time each time according to that day's time zone) run only the announcement line as a single request to confirm there is no new official announcement, and skip the full set of incidents/banked/local scan — announcement-type resets never fall in his sleep period (historical pattern, see the "Tibo's time notation" section); compensation-type ones do not depend on his schedule.Reducing frequency must not skip full-text interpretation of new entries: when Radar successfully returns an index item newer than the last adjudicated one, first read the full original post per §1, then decide whether to continue reducing frequency;official_window=nullmust not end early.Escalation exceptionsinclude:official_windowexplicitly gives a window; the body contains a future commitment; the time, type, or scope in the body would change the arrangement; or the ledger already has an undecided commitment that has reached follow-up conditions. If any one hits, escalate back to the full set and follow up according to the commitment's changed-call conditions; only if none hit, keep the single-request frequency reduction. State the cost clearly: if an unannounced silent reset happens during the sleep period, it will be delayed until the active period before being discovered — silent resets are also human-triggered, and the historical pattern supports that they will not happen during the sleep period, so treat it as acceptable. (On 2026-09-18 the user decided to keep the normal frequency-reduction branch.) Normal rounds and coverage self-review: the fixed check leg is the starting point of currently known sources, not a complete source table, and even less a guarantee of "not missing new resets." Each round handles new evidence, undecided questions, and feedback according to the clue decision below; even after consecutive rounds with no new signal, the next normal round must check whether any source, context, or counter-evidence worth investigating has appeared. When the user asks "are any signals being missed / how do others predict," directly check the source blind spots and the information available at the time; do not answer with "the full set has been run." Questions naming quota/balance itself go entirely through the Account SOP; this item only concerns reset status. -
What this Skill improves isOn each call or when the host has already scheduled a loopreads, judgments and records; it itself has no background scheduling, proactive notification or ACK mechanism. Without separately deploying and actually testing these runtime layers, one cannot claim to have guaranteed continuous discovery, delivery or read confirmation.
-
When the user asks "have all of our several accounts / been used up / there are still two at full quota / how many Full resets are left" → first read Per-account quota query and web login recovery。
-
When the user asks "what did Tibo say" or explicitly wants only official announcements → checkAnnouncement path。
-
When the user asks "when is the next one / will it reset tomorrow / is it worth waiting", including follow-ups right after a reset in the previous round → after checking announcements, readNext-round reset prediction, give the primary judgment, the basis and the update time. By default inherit the reset type from the context above; when explicitly asking about a personal cycle, still follow the account SOP, and do not use a personal weekly reset date to answer for the global prediction.
-
When the user says their weekly/5h is back to 100%,
Next resethas changed, or posts a usage screenshot → first record it as direct observation of that account, then check whether it is a personal cycle or a cross-account event. -
When the user explicitly says "it must have reset again" and the aggregator has no record → immediately follow thesilent reset path; it is forbidden to repeatedly query the same aggregator and then reject the user again with an empty result.
-
When the user only asks about the current remaining amount or the next cycle reset → likewise enter the account SOP's real-time query; when it is necessary to explain historical jumps, then follow §2's rollout snapshot. A current balance query does not require first running the entire history reconstruction or rechecking announcements.
-
To skip manual web login and connect the Google account already logged in in daily Chrome directly to an isolated query profile →
scripts/launch-usage-profile.sh <a|b>create/open an isolated Chrome profile,scripts/import-google-cookies.py --profile <a|b>transfer.google.comcookies (for the mechanism and security rationale see the script's own docstring, verified on 2026-09-15). -
Toautomaticallydrive isolated Chrome to check the second account's usage →
node scripts/read-usage-profile.cjs(depends on~/.chrome-profiles/tibo-cdp/'s playwright; default profile a). Read mode directly parses the currently logged-in account's usage; add--drive-loginto automatically clickContinue with Googlewhen not logged in, driving to the Google account selector and listing all accounts.Measured boundaries (2026-09-16): the isolated profile has no Google session token; the account selector marks every accountSigned out, so passwordless direct login is impossible—password/verification code is handed to a human, and only after the first login does read mode become fully automatic. Executable details encountered (proxy, which real input channel works for which button, login-state criteria, OAuth polling) see account-usage.md isolated Chrome automation section。
Output contract: give the conclusion first, then explain the boundaries
The user's exact words (2026-08-26): "You must give a conclusion instead of making me give the conclusion.」
- The first sentence of the first paragraph must give thesingle strongest conclusionsupported by the current evidence; it is forbidden to use "it might be A/B/C, please look again" to hand the classification responsibility back to the user.
- Uncertainty is used tonarrow the attributes of the conclusion, not to cancel the conclusion:
- Only one account confirmed → "that account has been reset; the trigger reason and scope of impact are unverified."
- Multiple accounts jumped early and the normal cycle cannot explain it → "an unannounced large-scale silent reset has occurred."
- Official explicit all/every → "officially confirmed a reset for everyone."
- Evidence, competing explanations, and fields pending verification go after the conclusion. When a given account's
Next resetor banked status cannot be obtained, still draw a conclusion about the known facts first, then explain which layer of attributes cannot be confirmed; it is forbidden to end with "check it yourself and judge." - An account's current value does not stand in for event attribution: current remaining,
Next resetand banked inventory only prove the state at query time. An account reading with an undetermined cause cannot serve as corroborating evidence that an officially announced reset has landed in that account; a natural cycle and an extra reset may occur in succession, check them separately per theaccount SOP, and do not use the earlier occurrence to explain away or deny the later one. - When the user asks follow-up questions about unfulfilled promises, delayed arrivals, or the event timeline, after the conclusion list in chronological order the nodes that would change the judgment: the outage and recovery (if relevant), the promise time and wording of the official original post, the account readings after the promise, and subsequent completion or explanation signals. Mark each node with its time zone, original source, and the scope it actually proves; keep missing nodes as unknown, and do not use the announcement time as the arrival time.
- Subsequent verification actions are only used to confirm/refute this conclusion, and must not be written as a multiple-choice question for the user to make the judgment on behalf of the agent.
- Questions about future times must give a predictive judgment. When there is a clear announcement, first report the converted official announcement window; when there is not, continue analyzing recent similar events and current signals, follow theprediction pathto give a well-founded main window, confidence, and conditions for revision. "Not officially announced / no fixed schedule / There is no schedule" only states the announcement status and cannot by itself end the answer. When the material is insufficient to support a date, give a judgment on whether waiting is worthwhile and the gaps, without inventing dates or precise probabilities.
Monitoring round: from information to actionable signals
Applies to bare calls, prediction inquiries, and normal rounds where the host repeatedly calls this Skill; when it is clearly only asking about account balance, only asking about the text of a known post, or attributing a past missed report, narrow the scope according to the entry-point routing. A reduced-frequency round retains the above trade-offs, but must not lose pending leads; when a pending lead reaches the review condition, it is escalated to a normal round. Each round executes in the following order, stopping when the evidence is already sufficient to change or maintain the current action, and what new information is needed next has been clearly written, rather than stopping at a fixed endpoint returning a null value.
- Continuing from the previous round's question. Read
forecast_log.py summary, first look atdue_for_followup(predictions whose window has expired or is about to close and are not yet settled — expiry follow-up items are read here at first glance, without digging through rationale; for the closing_soon threshold definition see forecast-feedback.md); if there iswithdrawal_conflicts, first check the retraction of the conflict and the old-version verification; if there isrecent_withdrawn, first rule out the retracted old window; also look atsnapshot.status——lagging, which indicates that the ledger's git snapshot is behind (for the cause and handling see the findings section of forecast-feedback.md), and this must be explained in this round's report; then from the same state-dir runforecast_log.py handoffto read the complete raw line of the latestinvocation=monitor;findingsThe compact list of the subcommand does not shownotes, and it cannot be used to replace the handoff body.handoffreturnsnullindicates that there is no monitoring handoff yet, and it cannot be inferred that previously there were no leads worth pursuing. Among them, thenotesrecords the specific unresolved issues from the previous round, candidate sources, conditions for the next review, and user feedback. If the previous round has no record, start from the current issue and the existing predictions'revision_triggerto establish these items, and do not describe an empty ledger as already covered. Even if this round only performs a reduced-frequency query, it must still, in the new finding'snotes, carry forward unresolved items or state which piece of evidence closed them, so that it can always be read back from the latest round; verbatim readings still go only inreadings, and judgments must not be disguised as raw data. Runforecast_log.py followupagain on the same state-dir, execute itsevent_tasksandaccount_tasks, and do not treat the output list as already checked.confirmed_unscoredindicates that the event has been confirmed, the prediction still cannot be scored, and do not keep asking "has the reset happened?" for it;account_followupseparately retains the not-yet-received items of already bound accounts. Store the specific officially announced time withannounce, and read fields and status from local prediction feedback。 - First take the increment, then ask what it changes. Using the Radar, Tibo main posts, replies, incidents, and community monitoring in §1 as the
baseline, compare the ID/time last read with the actual coverage this time; newly discovered old posts also enter this round's
interpretation according to theirfirst discovery time, and must not be discarded just because the posting time is earlier than the start of this round. Read the full body and the load-bearing parent / quote / image
text; mirrors and group screenshots of the same original post count as only one fact, but group screenshots should be retained as a new discovery path. First determine
"if this information is true, which judgment about timing, scope, type, receipt, or usage strategy would it change," and then decide
whether to pursue the next hop; do not use fixed keywords such as
resetor tracker tags as semantic gates. - Expand sources along the question, rather than scraping indefinitely. When a known source is blank, a future promise still has no independent evidence of fulfillment, the user asks about the reason,
the context points elsewhere, or a candidate would change action, first mark which of announcement, service restoration, account receipt, scope of impact, and execution reason
already have evidence, then list the competing explanations that could change the judgment. When a promise has no deadline, no change in this account for now is a pending-evidence status,
not a contradiction between the promise and the product; nor can it be asserted from this that the backend has not yet executed or has already been received in other accounts.
Choose the most discriminating next hop from post citations, reply targets, official incidents, independent account observations, and community records the user has authorized.
The user has pointed out that "VIP Group 1 | A Firework Community" is an effective supplementary discovery source for this case; if a normal round still has no fresh coverage of this group,
use the existing
read-wechat-messagesto read its increment, including pure images and voice, and then return to the original post to verify. Relevant groups will increase or decrease with user feedback and actual signals; do not write this group as a permanently complete source list. Group messages are discovery and cross-checking channels; the number of forwards is not independent evidence. For each hop, state the competing explanation it is meant to distinguish; when nothing can be found, retain unknown and the next executable review condition. When the evidence is already sufficient to answer the status or reason the user asked about, or when adding sources would not change the bounded conclusion, stop expanding; when there is no readable evidence for internal platform reasons, clearly retain unknown.followup.event_tasksmarked asoverdueWhen doing so, in the supplementary check list, verify separately the fresh account readings, the official main post, the candidate reply chains, bounded reply discovery, and the authorized community increments, checking respectively "whether the official has rescheduled or completed," "whether the same account has changed," and "whether other independent accounts have already received the credit." Looking only at the original announcement and the same batch of replies cannot complete this round of rechecking; if the channel is still unavailable, save this round's failure and the conditions for the next executable attempt, and do not treat the oldunknownas having been attempted in this round. The specific existing retrieval paths are still executed according to §1 and the community reading Skill, and the channel is not treated as permanently unavailable because of one failure.account_tasksthen only follow up on the corresponding account's credit, and do not reopen an already confirmed global event; unauthorized community sources remain uncovered. - Output in layers and preserve action differences. Verified events, future commitments, and action hints can constitute signals; vague replies, milestones, or third-party interpretations should first be listed as candidates, and if they would change the current arrangement, promptly flag their ambiguity and the conditions for further evidence, without upgrading them to explicit commitments. Clearly distinguish
global、banked、both, personal cycles, and unknown type:globalbefore arrival, consider bringing forward work that was originally worth doing;bankedfirst focus on credit arrival, expiration, and manual redemption, and do not recommend exhausting existing quota early just because it will be granted;bothfollow up separately on the two types of capacity; when the type is unknown, record the account baseline and prepare existing tasks, and make revocable conditional arrangements only when this account's readings and opportunity cost support it. When no new signal is found, only say "no new actionable information within this round's covered scope," and report it together with uncovered / unknown and the conditions for the next recheck, without saying there is no signal globally. If there is a new judgment in this round, use the original post, context, and product observations to explain what it adds compared with the previous round, and whether to notify, wait, or change the usage strategy; a send receipt must not be treated as a user ACK. The reverse also holds: when the official has explicitly announced that a global grant is in transit, wait for it to land before touching the backup—once the global grant lands, everyone is refilled, and redeeming the backup at that moment is as good as throwing away inventory (measured on 2026-10-11: 4 accounts all empty + EOD official announcement in transit; after the announcement it landed in 5h45m, still ~3h before the EOD upper limit, and 2 backups were preserved unspent). - Persist the state to be picked up in the next round. At the end of this round, append a handoff finding of
invocation=monitor;querywrite the current monitoring question,endpoints=[]、readings={},noteswrite this round's UTC check time, new clues, ruled-out explanations, unresolved questions, candidate sources, coverage of each source, next trigger conditions, and user corrections. For important judgments, additionally save or revise the prediction according to the prediction path, and useevidence_refsto link the original readings. Even if there is no new information, update the conditions that should be checked in the next round; do not on your own extend the host invocation interval on the grounds of "no signal for several consecutive days." A Skill can only specify behavior when invoked, and cannot prove by a single execution that future scheduled wake-ups, message delivery, or ACK are already in effect. After completing confirmation, appendreview, recording event confirmation and account readings separately. When there is only a completion post, still usetime_basis=confirmation_only, and do not fabricate an occurrence time or hit score just to clear the pending-check queue; the script will move records that are confirmed but unscorable toconfirmed_unscored. The official announcement time is a single-point record ofannounce, without adding a one-second tail to make up a prediction window, and without adding an execution grace period based on experience.
Verification Workflow
1. Announcement paths: Radar, Tibo main post, and replies
A normal round must actually attempt Radar, the Tibo main post timeline, and the bounded reply discovery described below. Radar may
not index a new standalone main post at all; the main post timeline also does not include replies, so the first two having nothing new still does not let you skip reply
discovery. When the host has no available logged-in X channel, the execution result of the reply leg is unknown, not a silent omission. Approved exceptions that are
rate-reduced by time slot are still executed according to entry routing: when a rate-reduced round only checks Radar, record both main_posts and replies as
uncovered, and do not write the result as a full no-new.
When this machine is logged in and twitter-cli is available, the main post entry uses the following read-only command (v0.8.5, measured 2026-09-23;
both help and JSON output were verified):
twitter user-posts thsottiaux -n 50 --json 2>/dev/null
The CLI's WARNING (such as Failed to init ClientTransaction) goes to stderr and does not affect JSON; using 2>&1 to merge streams will make
JSON parsing fail at the first character (measured 2026-09-30), so discard stderr.
Compare returned items by createdAtISO / id with the main post cursor of the last finding; read each new main post in full text, and preserve the
quotedTweet relationship. If the posting volume between two rounds exceeds the current window and the oldest returned item is still later than the last cursor, expand -n(up to
200) to fill the gap, and do not record a truncated window as covered.user-posts lists only main posts and cannot authenticate replies. Hosts without this CLI
use existing available Web/X original-site channels; when these channels are also unavailable, write main_posts=unknown, and do not fake complete coverage.
Radar continues as the structured index leg. Use the Tibo Radar JSON API to find the latest public announcements (measured 200 on 2026-08-26):
curl -s -m 15 "https://codex-reset.com/api/timeline" | python3 -c "
import json,sys
for e in json.load(sys.stdin)['events'][:5]:
print(e['announced_at'], '|', e.get('type'), '|', str(e.get('summary'))[:100])
ow = e.get('official_window')
if ow: print(' 窗口:', ow.get('label'), '=', ow.get('start_at'), '→', ow.get('end_at'), 'UTC')
print(' url:', e.get('url'))"
New entries first. Key fields:announced_at(UTC ISO)、summary(may be truncated),url(original post),
official_window、reset_verification_status。type is an internal lowercase value:reset = broad-domain
reset announcement,credits = banked/quota pack,boost/promo = consumption rule type.
After Radar successfully obtains a new index item, you must first read the full original post of that item, then judge whether it contains a future commitment or a
time, type, or scope that would change arrangements. The summary,type、official_window and verification labels are only used for locating and cross-checking; if any one of them
is empty, it still cannot short-circuit the full-text read. The full-text judgment result and pending fields are written into finding / forecast; only when the full text also has no commitment and the
ledger also has no due pending commitments can this round be recorded as "no new decision signal within the covered scope".
Summary truncation can also hide a preview: the latest item measured on 2026-09-08 had summary truncated only to the opening joke,
official_window=null、announcement_state=none, and the verification label was pending;
the full original post clearly previewed a global reset for all paid subscriptions,
with the time around 18:00 Pacific on the day of posting. Read the full text first before judging; do not declare "no new preview" based on index fields.
That post was at Beijing 09-08 03:24 and Pacific 09-07 12:24, so the preview converts to Beijing 09-08 09:00;
this is a conversion case for that preview, not a fixed schedule, and not evidence that it has already landed.
⚠️ type is a label applied by Radar editors, not a machine determination of the nature of the event—cross-platform interaction will also be tagged with
reset。 Measured on 2026-09-05: Tibo replied to Anthropic's Lydia Hallie (original post: "We've just reset
weekly limits for everyone on a Claude Max plan", Claude's official reset tradition), only replying with the joke
"Wow, huge, wonder why!", and Radar still gave it type=reset. Reading the original post is the only way to disambiguate;
the replying_to(the person being replied to) + replying_to_status(the id of the post being replied to) in the fxtwitter response are the fields prepared for
this step.
url Already in the output of the command above (printed directly since 2026-08-31, eliminating a second query); once obtained, read the original post first. When twitter-cli is unavailable (not logged in / not installed), the winning channel for the X post body is the fxtwitter public mirror API(Tested on 2026-08-30:
no login required, direct connection works, returns complete JSON;The complete body is in the tweet.text field—not full_text, that key
does not exist and copying it will cause a KeyError; the full text of long-form note_tweet is also provided, the 8-29 official announcement long-form post was tested and its full 2324 characters were obtained, ending
with a natural conclusion; tested on 2026-09-24 it can be entirely unavailable (12 consecutive failures), whereas the logged-in twitter-cli's
user-posts --json itself carries the complete body—rank channels by capability, don't hit fxtwitter first out of documentation habit):
curl -sS --max-time 20 "https://api.fxtwitter.com/<user>/status/<status-id>" \
| python3 -c "import json,sys; t=json.load(sys.stdin)['tweet']; q=t.get('quote'); print(t['created_at']); print('reply_to:', t.get('replying_to'), t.get('replying_to_status') or ''); print('quote:', json.dumps(None if not q else {'id':q.get('id'),'url':q.get('url'),'text':q.get('text')}, ensure_ascii=False)); print(t['text'])"
Fallbacks and dead ends (tested on the same day):
cdn.syndication.twimg.com/tweet-result?id=<id>&lang=en&token=a(official endpoint, 200):body truncated to 276 characters, note_tweet only gives the id and not the content—can only be used to verify the beginning and metadata, long-form text cannot be obtained.publish.twitter.com/oembed: 301 falls topublish.x.comthen (adding-L) returns 200+JSON, but the visible text is likewise truncated (~273 characters, ending with an ellipsis, truncation point same as syndication)—same tier as syndication, good enough to verify the beginning and metadata, cannot obtain the full note_tweet text.- Jina Reader usable but intermittent, do not rely on it as the primary channel: anonymous access to x.com will, due to others' abuse, beintermittently globally blocked(403; tested on 2026-08-30: the block was lifted several hours later, and after lifting, anonymous access could still obtain the post body; the error message names the third-party account that triggered the abuse); this repo's jina key already has 402 insufficient balance. Prefer fxtwitter, use Jina only as its backup.
- fxtwitter does not return replycontent(
repliesfield is only a numeric count). Thereplying_to(the replied-to person's handle) andreplying_to_status(the replied-to post id) it returns are enough to determine whether a known post is a reply; the replied-to post itself can be fetched once more with the same command; but itcannot enumerate which replies exist under a main post. Zero hits on the main post, no new items in the main post's timeline, or some CLI search returning 404 must not be written up as "there are no replies". - For verifying the reply chains of known candidate posts (announcement posts, landing posts) there is a cheaper route:
twitter tweet <id> --jsonreturns in one go the main post + replies (the author field isdata[].author.screenName—notuserName, tested on 2026-09-24 50 items, and based on this it was confirmed that under the commitment post Tibo himself had zero replies). It only gives "the replies under this one post"; the discovery scan for "whom has he recently replied to" still uses the web-page scrolling method below.twitter search(including thefrom:query) tested as HTTP 404, the search route is unusable. - bounded reply discovery path(tested on 2026-09-23, while logged in on the X main site, it was possible to see an old reply missed by the main post entry):
- First fix the starting point: the UTC of the lastreliable reply check; if none, use this task's window or the starting point of the current pending commitment.
Open
https://x.com/thsottiaux/with_replies, and record this round's URL, starting point, and start time. - Record only the entries actually displayed on the page whose author is Tibo: save the status ID and UTC one by one, and keep scrolling toward older content until the page has passed the starting point, or an explicit loading stop / resource limit / site failure appears. Scrolling past a time boundary on the page only means that point was observed,it does not prove the interval is exhausted; covered must not be written on that basis.
- Once a clue is found that would change the schedule's commitment, timing, type, or scope, you may stop scrolling further into older content, turn it into a candidate, and gather supporting evidence.
For each important candidate, use the fxtwitter single-post command above to read the body text,
replying_to_statusand thequote's id / URL / body text. If a parent exists, use the same command to read the parent; if a quote exists and its content is load-bearing, then read the original post by the quote URL.quote: nullis a healthy no-quote form, not unverified; only when an actually existing load-bearing node cannot be retrieved does the corresponding judgment remain unknown. - The finding records
window_start/window_end, the observed status IDs, the oldest visible UTC, and the stop reason:boundary_observed/loading_stopped/resource_stop/site_failed/not_logged_in. No logged-in state or site failure meansunknown; reaching the boundary but having only web-scroll evidence meanspartial, and a negative result may only be written as "no relevant clues among the replies observed this round".
- First fix the starting point: the UTC of the lastreliable reply check; if none, use this task's window or the starting point of the current pending commitment.
Open
- Each round, write reply coverage per object:
covered(candidate_chain:<id>)is used only when the body text of one known candidate and its actually existing parent / quote load-bearing nodes have been fully verified, or some interface provided a genuine exhaustion signal and an explicit scope was recorded;partial(<window>)is used for bounded web observation;uncoveredmeans that this round had only main-post/mirror entry points that structurally contain no replies;unknownmeans the reply channel failed or is unavailable. The latter three cannot declare that there were no replies in that period or no new signals globally. A main-post entry point always counts only as main-post coverage, and is not upgraded to reply coverage because it returned zero items or returned the full body text. - The reply chain of the landing confirmation post must be read; the number of tracker entries does not equal the number of events(measured 2026-09-10): under the 09-08 "All reset for everyone" official announcement post, Tibo replied "You forgot the part where I reset usage twice in the middle" — before the formal landing, there had already been two mid-way global resets that day, and this account exists only in the reply. codexrunway marked this reply asa second independent Completed Global reset (Confidence 93%). A teaser + mid-way escalation + landing is one round of events (for the merge rule see next-reset-forecast); read replies with the same fxtwitter command; the reply post id is extracted from the x.com status link in the tracker page (on 2026-09-10 it was obtained by grepping codexrunway's static HTML), and fxtwitter itself has no replies list.
codexlimitwatch.com/codex-reset-history and Radar both take Tibo activity as their core upstream and belong to thesame source
family, so they can only cross-check each other for transcription/parsing consistency and cannot be called independent dual sources.LunarWerx Codex Forecast
(codex.lunarwerx.com) lies between the two: itsverification legis independent (the site states so itself and the meta description was measured as
"checked against OpenAI's own status page"), but itsdata upstreamis still the public reset record (including Tibo activity;
the community describes its model as driven by Tibo hints) — so it is suitable for "third-party cross-validation of theinterpretationof Tibo signals"
(2026-08-30: for the celebration post it independently gave the same "reset tomorrow" reading, marked 95% confident),
cannot serve as a second source that "independently observed the reset event", otherwise it commits the same-source error this skill warns about. It is an SPA; static scraping can only get the meta and the shell, and the body content is referenced via WebSearch. The HTML
codex-reset.com/tibo is an SPA, and the static content may lag; treat it only as a human view.
Third-party prediction leg:www.codexrunway.com/api/status.json redirected on 2026-09-26 to
didcodexreset.com/api/status.json; after each retrieval, verify the final URL,generatedAt、
lastSuccessfulCheckAt、monitor.status and the current fields, and do not assume the old domain or old fields remain valid permanently. The current response uses events[] of kind to distinguish reset_scheduled from reset_completed, and does not have the
Expected next reset field from the old description. It is still a same-family interpretation of public information such as Tibo's, not an independent account observation. If the direct JSON connection fails, retry at most 3 times; to get the most recent completed event you can use
[e for e in d['events'] if e.get('kind') == 'reset_completed'], sorted by announcedAt .
Prediction times must be checked against the original post: for each reset_scheduled compare announcedAt、effectiveAt and the time wording of the original post. The 2026-09-26 response marks an open-ended future-tense promise as scheduleBasis=explicit,
effectiveAt even earlier than the original post's announcedAt; when the effective time is earlier than the source post or the original post has no time limit, keep the promise as pending fulfillment and discard the date given by the site.reset_completed is also just a site classification, and you must go back to the original post to find completion wording or verify with product readings; filtering by status=='completed' is not an event criterion.
First check resetType and source.origin before using this event:resetType is global or banked,
banked and is not used to verify global predictions.source.origin=operator indicates manual entry by the site operator with no X original post (insource there is only the internal postId, with no handle and no x.com link); events from X sources do not have the origin key. Both types may be operator-sourced (global example: 2026-08-25 "Operator-confirmed … without an X announcement"). Treat such events as candidates: the banked arrival of the 2026-09-29 DevDay was initially in exactly this form, with the site describing the source as the operator rather than the original post, and it was only confirmed after the user confirmed that multiple accounts under their name had all received it. The basis for upgrading to arrived differs by type:banked events — the official original post, or the user's direct observation of their own account, or before-and-after readings of the banked inventory of the same account_ref ;global events — the official original post, or usage-refilled readings from multiple accounts after excluding natural cycles. When there are no prior readings, report only the current value: for banked write "arrival unverified", and for global write "occurrence unverified".
Radar cannot index the official incident line — this is a structural blind spot of the announcement path. Radar only indexes @thsottiaux, while ChatGPT/Codex incidents are posted by the @ChatGPT account and status.openai.com . Resets may also be used for incident compensation; checking only Tibo's posts will miss this clue.Check the incident line together before every answer.
# 官方状态页(2026-09-01 实测 200,返回 Partial System Degradation + 未解决事故)
# 重试是必须的,不是保险:不带重试的裸命令实测会间歇吐 JSONDecodeError 而不是「站点挂了」
for i in 1 2 3; do
o=$(curl -s -m 20 -A "Mozilla/5.0" "https://status.openai.com/api/v2/summary.json")
if printf '%s' "$o" | head -c1 | grep -q '{'; then
printf '%s' "$o" | python3 -c "
import json,sys
d=json.load(sys.stdin); print(d['status']['description'])
for c in d.get('components',[]):
if c.get('status')!='operational': print(' 异常组件:', c['name'], '→', c['status'])
for i in d.get('incidents',[]): print(' 未解决事故:', i['name'],'|',i['status'],'|',i['created_at'])"
break
fi
echo " attempt $i 空响应,重试中"; sleep 3
done
For @ChatGPT posts, use the same fxtwitter command, just replace <user> with ChatGPT .All external endpoints in this section (Radar, fxtwitter, status page) will intermittently flake— when tested locally through a proxy, within the same minute status.json returned empty while summary.json succeeded, Radar's first run returned an empty response body and directly JSONDecodeError, and fxtwitter twice in a row
SSL_ERROR_SYSCALL succeeded on the third try (independently retested on 2026-09-07) —Failure: retry 2–3 times first before judging the endpointunavailable, a single failure does not constitute "the site is down".
⚠️ summary.json Only looking at whether it is currently green cannot reveal historical incidents—you must also check incidents.json。
Running only the summary will miss already-ended compensation-type Codex/Work incidents (e.g., 09-14 02:46 UTC "Elevated
error rates for Codex and ChatGPT Work") andsilent quota anomaly investigations(e.g.,
09-09 17:29 "Investigating unexpected usage limit resets", body: "Some Codex users may be
experiencing unexpected usage limit resets"). The latter directly concerns quota, and its name does not contain
Codex; filtering only by Codex/Work name will miss it. Each round, run the following together with the
summary:
# 近期 incident + 完整正文(summary.json 看不到)。按补偿/额度语义标记:
# [补偿型] 名字含 Codex/Work
# [静默型] 名字或正文含 usage limit / unexpected reset / billed / quota(不依赖 Codex 命名)
for i in 1 2 3; do
o=$(curl -s -m 20 -A "Mozilla/5.0" "https://status.openai.com/api/v2/incidents.json")
if printf '%s' "$o" | head -c1 | grep -q '{'; then
printf '%s' "$o" | python3 -c "
import json,sys,re
d=json.load(sys.stdin)
silent=re.compile(r'usage limit|unexpected.*reset|billed|billing|quota|payment',re.I)
for inc in d.get('incidents',[])[:14]:
name=inc.get('name','')
body=' '.join((u.get('body') or '') for u in inc.get('incident_updates',[]))
f=''
if 'Codex' in name or 'Work' in name: f+=' [补偿型]'
if silent.search(name) or silent.search(body): f+=' [静默型-额度]'
print(f\"{inc.get('created_at','')[:16]} | {inc.get('impact',''):6} | {inc.get('status','')} | {name[:52]}{f}\")"
break
fi
echo " attempt $i 空响应,重试中"; sleep 3
done
When a candidate is hit, use incident_updates[].body to read the body and then judge: containing compensation/reset/quota wording → escalate to a strong signal (compare
the 09-12 "reset is also landing by midnight today"); pure error-rate fluctuation without compensation wording → only record as a candidate.
impact: none And those recovering in <2h with no quota semantics in the body are ignored as noise.
③ Community post classifier, included in routine polling. It tracks @thsottiaux posts and can surface posts missed by Radar or different interpretations; it cannot independently discover a silent reset with no post, only account quota changes. The latter goes through the account and community live testing in §3. Each round, run it together with the above (upstream and mirror are same-source, used only for interpretation cross-checking and not adding independent counts; first verify successful check and time, then read Yes/No):
# 社区帖子分类器(提取候选与检查时刻,按下文判读)。http!=200 就跳过,不阻塞。
for u in "https://hascodexratelimitreset.today" "https://lidless.app/did-codex-reset-today"; do
f="/tmp/tibo_c_$(echo "$u"|md5).html"
code=$(curl -sS -m 15 -A "Mozilla/5.0" -o "$f" -w '%{http_code}' -L "$u")
[ "$code" = "200" ] || { echo " $u http=$code 跳过"; continue; }
python3 -c "
import re,html
t=open('$f',encoding='utf-8',errors='replace').read()
txt=re.sub(r'<script.*?</script>|<style.*?</style>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',txt))).strip()
m=re.search(r'(No sign.{0,80}|Verdict: (Yes|No))',txt)
mirror='lidless.app' in '$u'
unready=re.search(r'No classification yet|Awaiting first pass|Waiting for first tracked post',txt,re.I)
checked=re.search(r'Last checked.{0,70}',txt,re.I)
if mirror: verdict='unknown: mirror requires upstream and time check'
elif unready: verdict='unknown: first pass incomplete'
elif not checked: verdict='unknown: check time unavailable'
else: verdict='unknown: verify check time'
print(' monitor:',verdict[:90],'| candidate:',(m.group(0) if m else 'none')[:90],'| check:',(checked.group(0) if checked else 'time unavailable')[:90])"
done
No classification yet / Awaiting first pass, a missing verifiable check time, a check earlier than this round's load-bearing announcement, or
after timezone conversion later than the current clock, all record unknown; the mirror only shows candidate wording and cannot substitute for a successful upstream check.
When the upstream check time is valid and the candidate wording changes to Yes, or the lidless wording changes from "No sign" to confirmed and is verified upstream,
first find the original post it cites, then verify with the live API and contemporaneous account live testing; if there is an official reset announcement, judge by the announcement path; if there is no announcement but
an account anomalously resets to zero, then go through the §3 silent reset path, all named by the scope of evidence and not extrapolated to everyone. Candidate No / unable to parse only means
this post classifier did not give a positive, and does not close an unfulfilled official announcement, account anomaly, or a reason users are currently pressing about.
2. Local forensics: Codex rollout snapshots = scriptable anonymous usage evidence
~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl written during Codex operation to rate_limits
snapshots, which can trace back the usage change intervals observed on this machine. Snapshots have no account identity, and do not cover accounts that did not run Codex;
per-account current values and identities still rely on the verified product page or live API. Field shape:
{"limit_id":"codex","primary":{"used_percent":76.0,"window_minutes":10080,"resets_at":1788753995},
"secondary":null,"credits":{"has_credits":false,"balance":"0"},"plan_type":"pro"}
The commands in this section target the default ~/.codex home page; when using a custom CODEX_HOME , uniformly replace the auth and sessions paths in this section
with the same authorized home page; do not mix snapshots from two home pages.
resets_at is epoch seconds;window_minutes 10080 = weekly window, 300 = 5h window. In the snapshot,
credits and the distinction from backup resets, seethe field-reading method in the account SOP。
If you want the backup reset count, do not look for it in this snapshot: that key only has balance/has_credits/unlimited,
it does not carry the banked count. This is "changing the source",
not "this time it was not found"—directly run scripts/query_usage.py。
will give a trap with plausible wrong answers (none of them report an error; 1–3 were hit consecutively in the same session on 2026-09-01, 4 was added on 2026-09-03):
primaryslots do not fixedly point to the weekly window. In contemporaneous snapshotsprimarySometimes it is 300 (5h). Sort bywindow_minutesbuckets, do not assumeprimary= weekly—reading them mixed together will treat the 0% of a 5h window as a weekly quota reset.limit_idThere are multiple buckets, among which are constant-zero decoys. In actual measurements, during the same period there existscodex、premium、codex_bengalfox, whilecodex_bengalfoxof the two windowsare always 0%, and mixing them into the sequence will fabricate dozens of "resets" out of thin air.Firstlimit_id == "codex"filter before making any judgment.resets_atEvery snapshot has sub-second micro-drift. Using "resets_atchanged" to determine a reset will yield hundreds of false positives; the zeroed list uses a large-drop threshold defined by the script. An extra reset just after a natural cycle may drop only a few points and is listed separately as a "low-usage anchor advance" candidate; when the sampling interval is long, the next snapshot may already have consumed a fair amount of quota again. An anchor advance may also be a natural cycle, account switching, or concurrent sessions, and neither the candidates nor the zeroed list directly proves a platform event. A miss in both also does not prove there was no reset.- Directory date ≠ timestamp range—counting days by
sessions/<Y>/<M>/<D>/will silently under-sample. A long session crossing midnight writesthe next day'stimestamps continuing intothe previous day'sdirectory, so "scanning the most recent N date directories" does not fully capture the most recent N days; short-term changes interleaved across multiple accounts may also be missed because of this. Fix: runscripts/scan_rollouts.py; it additionally covers cross-day directories, then trims by actual timestamps.
The shape of the window anchor—note that "clean +7d" itself is not evidence of a reset:
- Clean +7d: new
resets_at≈ zeroed moment + window length. Thisonly shows that the window restarts from the zeroed moment, it is at the same time the shape of a button-style reset and of "switching to another account that has quota"—the two are indistinguishable along this dimension. To determine the nature, you must go through the multi-account attribution check below. - Not clean +7d: the new anchor is not close to that snapshot's moment plus one week; a long sampling interval can also produce this label, and it does not indicate whether the anchor moved forward or backward relative to the previous one.
- Anchor backward shift: the new anchor is earlier than the previous snapshot's anchor. On multi-account machines, switching back to an account whose window opened earlier will also produce this shape; first rule out the account, then discuss window configuration switching.
- Account switching: see the multi-account check below. It can masquerade as any of the above.
Concurrent sessions will cause the same change to output multiple zeroed records. Lagging sessions report the old value first, then each updates. The zeroed list is not deduplicated; adjacent rows with the same anchor must be checked side by side, and must not be mechanically added together or directly treated as the same account. Low-usage candidates have separate deduplication logic, and the row counts of neither list are the number of confirmed resets.
⚠️ Do not treat "concurrent session lag" as a catch-all explanation—it is specifically used to hide multiple accounts. Deduplicating and merging by anchor combines
two records with the same new anchor, and mechanically cannot touch records with different anchors; the real risk is inthe judgment layer: upon seeing two records that are close in time
but contradictory in value, casually attributing them to "lagging sessions" will miss the clues of interleaved accounts.
used_percent is the used amount; normal use will naturally increase it. When it rises sharply within a short interval and is accompanied by anchor rollback,
verify identity, sampling interval, session source, and quota configuration; do not rule out other explanations based on shape alone.
When usage rises and is accompanied by anchor rollback, the clue is in the "bounce-back" output of step 1, not in the zeroed list that only captures sharp drops.
Read the bounce-back first, then the zeroed intervals and low-usage candidates; only after verifying identity and period item by item can you count confirmed events.
Multi-account attribution check
The rollout snapshot used in this sectiondoes not record account_id; these identity-free snapshots alone cannot distinguish account interleaving from a real reset.
First, make one structural trap clear: the intuitive check always returns "only one." ~/.codex/auth.json
only savesthe currently logged-in oneaccount; switched-away accounts leave no trace;~/.cc-switch/cc-switch.db only records the entries it has
managed itself, and accounts switched manually with codex login are completely invisible to it.Using the current state of these two places to answer "how many accounts
have been used historically" is answering a historical question with a current snapshot—it will not error, it will only give a plausible wrong answer.
(Measured on 2026-09-03: on a machine that indeed had two Pro accounts alternating, both probes reported "only one," causing the entire
attribution to be written backwards.)
Collect clues using checks A/B/C below, then name them according to the evidence scope in §3. Identity inconsistency proves the machine has used multiple accounts; to determine the cause of a particular jump, the readings before and after still need to be bound to specific identities.
Finding no anomaly alsocannot prove a single account: A only keeps the most recent refresh time, B does not cover manual logins, and C cannot see account alternation whose anchors happen to be monotonic. If the user has not yet stated whether the account was switched during that period, and the answer would change the historical attribution, ask once more; do not repeatedly ask about facts already confirmed. When identities cannot be separated, report the attribution limitations of mixed records, and do not treat all-green checks as proof.
Related impact: logging in/restoring an account revokes the same account's refresh token held elsewhere, and the only ones crying out are the "old processes."
A long-running Codex TUI caches the token in memory and does not update with auth.json ; after the old token is revoked,
only these processes refresh Failed to refresh token: … signed in to another accounton their own timers every 30–70 seconds—the number of error entries = number of processes × retry frequency, not "that many problems occurred." The fix is to restart
these old processes (which read the new auth.jsonat startup), not to log in again globally; to locate which processes are refreshing, check
~/.codex/logs_*.sqlite(take the one with the latest mtime; when CODEX_HOME is customized, it is <主页>/logs_*.sqlite) and count by process_uuid(which contains the pid).
(Measured on 2026-10-03: among 25+ Codex processes, only two started 9 days earlier were refreshing, 164 times each, and it stopped after restart.)
Execution order: first run the reconstruction script at the end of this section, to obtain the zeroed intervals and C's bounce-back; then read A, and read B only if the user indeed uses cc-switch. A's refresh times need to be aligned with the zeroed intervals.
Layer A — current identity + (indicative) last refresh time
Read identity and refresh time separately; the two have different evidential strength:
- Current logged-in identity(
id_tokenthe email /chatgpt_account_id/ plan parsed out)—this is a hard fact, and is used for identity comparison when layer B applies. last_refreshtime—Indicative, not a verdict:- Semantics uncalibrated: the field is literally named refresh, and token renewal also writes to it. It falls within the zeroed-out interval, so it still cannot be distinguished from a pure token renewal; for the attribution conditions see §3.
- Coverage is a single point: it is a single timestamp, earlier refreshes have already been overwritten, and it cannot be used to explain the entire history.
- Not falling within the interval ≠ that layer is clean: it only means "the most recent refresh is not in this interval"; earlier switches were long ago
overwritten (this is exactly the consequence of
auth.jsonstoring only the current state).
python3 -c "
import json,os,base64
d=json.load(open(os.path.expanduser('~/.codex/auth.json')))
t=d.get('tokens') or {}
print('last_refresh:', d.get('last_refresh'), ' <-- 与归零区间对齐')
print('account_id :', t.get('account_id'))
idt=t.get('id_token')
if idt:
p=idt.split('.')[1]; p+='='*(-len(p)%4)
pl=json.loads(base64.urlsafe_b64decode(p))
a=pl.get('https://api.openai.com/auth') or {}
print('email :', pl.get('email'))
print('plan_type :', a.get('chatgpt_plan_type'))
"
Layer B — which other accounts have also been stored on this machine (checked only when the user actually uses cc-switch)
This is an optional clue for historical attribution, not an entry point for taking account inventory. If the user explicitly does not use cc-switch, skip this layer and go to the official website and
Google logged-in accounts. (For the entry-point ruling on per-account quota queries see account-usage: as of 2026-09-08 this batch of accounts has
been ruled not to be managed with CC Switch — the providers records in Layer B serve only as email clues, and this ruling is not reopened for querying quotas.)
The following providers only proves the identities that have appeared in its records; it cannot prove that the account list is complete.
profiles table may in practice be empty (0 rows on that machine on 2026-09-03); accounts exist in providers ; each entry's
settings_config embeds a complete auth object, and only by parsing its id_token can you get the identity.
python3 -c "
import sqlite3,os,json,base64
db=os.path.expanduser('~/.cc-switch/cc-switch.db')
if not os.path.exists(db): raise SystemExit('no cc-switch.db (不构成单账户证据)')
c=sqlite3.connect('file:'+db+'?mode=ro',uri=True)
for pid,name,cur,cfg in c.execute(\"select id,name,is_current,settings_config from providers where app_type='codex'\"):
auth=(json.loads(cfg).get('auth') or {}); tk=auth.get('tokens') or {}; email=None
if tk.get('id_token'):
p=tk['id_token'].split('.')[1]; p+='='*(-len(p)%4)
email=json.loads(base64.urlsafe_b64decode(p)).get('email')
mode=auth.get('auth_mode')
kind='ChatGPT 账号(计入)' if mode=='chatgpt' and email else 'API-key provider(不是账号,忽略)'
print(f'{name!r} is_current={cur} auth_mode={mode} email={email} <- {kind}')
"
Interpretation rules (counts only genuine ChatGPT subscription accounts):app_type='codex' has API-key providers mixed in underneath
(measured on that machine: one DeepSeek,auth_mode=None、email=None) — that is not a ChatGPT account,does not participate in
account counting. Look only at rows where auth_mode='chatgpt' and email is non-empty.
- In such rows, the appearance ofan email different from Layer A → direct evidence of two accounts (both measurements on 2026-09-03 and 2026-09-04 hit it this way).
- Only one row and it matches Layer A → this layer has no positive evidence.
email=Noneorauth_modeis notchatgpt→ ignore, do not compare it with Layer A for "inconsistency".
⚠️ is_current=1 is not the criterion for "currently logged in". It only indicates who cc-switch itself last switched to; if the user bypasses
it and manually codex login , this marker will not update. Measured on 2026-09-04: on that machine the email pointed to by is_current and the actual logged-in email shown by Layer A auth.json aretwo different real accounts— the same
reading simultaneously demonstrates this trap and the hit pattern of Layer B (the inconsistency between the two emails is itself direct evidence of multiple accounts).
The current CLI login identity is determined by Layer A; the web identity is verified separately from the web. is_current only answers who the old tool recorded.
Layer B being quietdoes not mean a single account— it cannot see manual codex login。
Layer C — behavioral signature: anchor jump-back (relies only on rollout; still valid when both A and B fail)
The jump-back check does not depend on auth files, but it only detects snapshot shape and does not output account identity. The window configuration, sampling interval, and account interleaving all need to be verified; interpretation:
-
回跳次数 = 0→ no positive evidence (does not equal a single account, see the gate description at the beginning of this section) -
Rebound band used% rising → flag interleaving/configuration clues that need verification; do not directly write them up as multi-account facts.
-
Largerebound → prioritize checking account switching and quota configuration changes; for attribution conditions see §3.
-
A rebound exists butis neither large nor accompanied by a used% rise(middle band) → keep it pending verification; do not ignore it, and do not directly characterize it either. In this band, quota configuration switching and account switching are genuinely indistinguishable; do not silently let it pass just because it "doesn't look large enough."
-
"Same minute + same anchor + different used%" cannot be judged as account interleaving: polling lag of concurrent sessions also produces this combination; do not count it as an independent account clue. A low-usage anchor moving earlier is likewise just the shared shape of resets, natural cycles, and account switching, not a new account-identity detector.
A free account fingerprint — in the usage-limit error, the try again at
When hitting the limit, Codex writes a task_complete error, with a body of the form:
You've hit your usage limit. Visit <codex usage settings URL> to purchase more credits
or try again at Sep 7th, 2026 3:23 PM.
try again at is the reset time reported by the login context that produced the error at that time.Non-monotonic regressioncan suggest account interleaving,
but it carries no identity, nor can it alone rule out window configuration changes. The following
command scans the full history and marks regressions itself; on the tested machine it printed 17 times(when two buckets alternate back and forth, each switch is recorded once,
so it is an indicator of "whether there is alternation," not a count of "how many times accounts were switched"). The hardest spot is 08-25 14:46
four different values appearing within the same minute — several concurrent sessions each attached to a different account hitting the wall at the same time. (It is not "clustering into two groups": each reset generates a new window, so the values are always changing anyway; the number of clusters is not the signal, monotonicity is.)
⚠️ Before using the fingerprint to support a specific attribution, first verify whether the regression time falls within that interval. The sequence is monotonic in segment A and has a regression in the earlier segment B; this does not constitute evidence of account interleaving in segment A — "this machine has switched accounts historically" and "this jump was caused by an account switch" are two propositions; sharing the output of one command does not mean sharing evidence. This was actually hit in testing (2026-09-16): values from 09-09→09-15 were cited to argue for account interleaving in that interval, but in fact that interval had zero regressions and monotonically increasing values, with all regressions in 08-25→09-08, i.e., before the interval being argued about. The cost of this kind of error is not in miscalculating the numbers, but in writing a jump with "cause unverified" as a certain attribution —this is exactly why §3 requires naming by evidence scope first。
python3 -c "
import json,glob,os,datetime,re
BJ=datetime.timezone(datetime.timedelta(hours=8))
T=lambda x: datetime.datetime.fromisoformat(x.replace('Z','+00:00')).astimezone(BJ)
rows=[]; prevdt=None
for f in sorted(glob.glob(os.path.expanduser('~/.codex/sessions/*/*/*/rollout-*.jsonl'))):
for line in open(f,encoding='utf-8',errors='replace'):
if 'usage limit' not in line: continue
try: d=json.loads(line)
except: continue
e=((d.get('payload') or {}).get('error') or {})
m=e.get('message') if isinstance(e,dict) else None
if not m or not d.get('timestamp'): continue
v=m.split('try again at')[-1].strip().rstrip('.')
try: dt=datetime.datetime.strptime(re.sub(r'(\d+)(st|nd|rd|th)',r'\1',v),'%b %d, %Y %I:%M %p')
except Exception: continue
rows.append((T(d['timestamp']),v,dt))
rows.sort(); prev=None; back=0
for t,v,dt in rows:
if v==prev: continue # 只看取值变化
mark=''
if prevdt is not None and dt<prevdt:
back+=1; mark=' <-- 非单调回退:需核对账号或窗口配置'
print(f'{t:%m-%d %H:%M} | try again at {v}{mark}')
prev=v; prevdt=dt
print(f'\n非单调回退次数: {back} (只报告形状,不证明账号数量或归零原因)')
"
Zeroing can only report an interval, not a moment. Adjacent snapshots may be hours apart; write "landed between A–B," and do not treat "the time of the first snapshot showing 0%" as the arrival time.Also, snapshots only update up to the moment the user last ran Codex— before concluding "there has been no reset so far," first check how old the latest snapshot is; after that is a blind spot. Reading the live API or product page per the account SOP can confirm the current state, but it cannot fill in past historical blind spots.
rollout also has a coverage boundary: each one only observes the login context that produced it; the historical set may mix multiple accounts, and there is no per-entry identity label. Other accounts that have never run Codex have no snapshot, and it is not possible to infer all accounts from the last row of the set. The per-account product page readings relayed by the user are first-hand observations of the corresponding account; when citing a relative countdown, convert it to an absolute time and note the observation time used for the conversion.
query_usage.py of reset_at is consistent with the scan tail anchor, but only the window time matches; it cannot attach an account identity to an anonymous snapshot. When determining the current CLI account, rely on API or product page readings verified against the authenticated identity. When the identity of a historical segment cannot be established, retain the attribution limitation of the mixed sequence.
# 重建本机周额度曲线 + 多账号回跳与低用量锚点前移候选
uv run python scripts/scan_rollouts.py --days 7
# 复现历史某时刻的切面(两端均按时间戳裁剪):加 --as-of "2026-09-12T17:44:00+08:00"
# 自定义 CODEX_HOME 时:--codex-home <已授权主页>(auth 与 sessions 必须同属一个主页,不混用)
3. Silent reset path: check account facts, rather than continuing to wait for posts
When direct user observations conflict with the announcement index, gather evidence in order:
- Determine account facts: record whether weekly and 5h have returned to 100%, whether
Next resethas moved, whether the banked reset is still present, and whether the change occurred exactly at a previously displayed normal reset time. The product usage page or/statusonly proves that account, but the evidence level is higher than an aggregator's empty result.Product page observations relayed verbally or in chat by the user(multi-account quota statistics, banked balance, "there are N full resets")are recorded as direct observations, on the same level as the product page—there is no need to force the user to take another screenshot or run a command just to "see it with your own eyes."If this machine has~/.codex, run §2 first: it provides historical observation intervals and anchor candidates; whether it is explained by a normal cycle should be checked according to theaccount SOP by comparing readings before and after for the same account. - Find contemporaneous real-world reports: search recent posts using the current UTC/PT date, for example
Codex reset today back to 100%、Codex reset again 5h、site:reddit.com/r/codex reset today. Prefer screenshots, clear before-and-after percentages,Next resetchanges, and "banked still present"; reposting the same message does not add independence. If the user says "saw it in the group" and the current environment has group chat archiving capability, then search "reset/Tibo" to cross-check group members' real-world reports, and distinguish whether the screenshot is product status or a Radar repost. - Find publication context: search whether Tibo/OpenAI is switching 5h/weekly limits, fixing metering, or handling an incident. Context occurring at the same moment as the reset only supports causal inference; if the official source did not say "therefore reset," mark it clearly as inference.
- Name by evidence scope:
- First rule out account rotation. Inconsistent A/B identities prove the machine has used multiple accounts, not that a particular zeroing was necessarily caused by switching accounts;
last_refreshfalling within the interval can only serve as a hint. Only when the readings before and after that instance can be bound to different identities may it be called "this was an account switch"; otherwise write "mixed account records, cause of zeroing unverified" and do not upgrade it to a platform reset. - Only one account → "this account has been reset; cause undetermined," and it cannot be extrapolated.
- Multiple different accounts refill within a very close time, but their respective normal cycles have not yet been ruled out → "near-simultaneous resets across accounts observed; whether it is the same platform event is undetermined."
- Neither advance notice
Next resetnor normal cycles for multiple independent accounts can explain this early jump → "large-scale silent reset observed"; same-period limit announcements can only supplement context and cannot replace this counterevidence. - Only when the official source explicitly writes all/every paid account → may it be called a "reset for everyone."
- First rule out account rotation. Inconsistent A/B identities prove the machine has used multiple accounts, not that a particular zeroing was necessarily caused by switching accounts;
When a silent event has no officially announced timestamp, report that it "occurred no later than the time of the earliest public evidence"; do not disguise the posting time as the precise moment it landed.
4. Announcement time conversion
official_window When it exists, read its start_at/end_atfirst; then convert it yourself once according to the rules below.
If they disagree, report the discrepancy. The operating system time zone database determines the offset for how a given time zone is interpreted, not which
time zone the author actually used; absent clarification from the person themselves, retain the load-bearing literal interpretation and the primary interpretation, and do not directly call the tracker's alternative interpretation a miscalculation.
5. When a channel fails
Radar API down → twitter-cli (when logged in:user-posts fetch the main post timeline and full body text,tweet <id> --json
fetch the known post reply chain; on 2026-09-24, when Radar returned empty responses 4 times in a row and fxtwitter failed 12 times in a row in live testing, it alone propped up both the announcement and
reply legs) → fxtwitter to read the original post (the command in §1) → the official syndication endpoint (truncated to 276 characters, only enough to verify metadata) →
codexlimitwatch single source (labeled as a same-source mirror) + LunarWerx (only for cross-verifying Tibo signal interpretation, not an independent second observational source) →
WebSearch thsottiaux reset
to find a transcript. When a user reports that the product has changed, even if all announcement channels are empty you must still follow the silent reset path; only when all product/community
evidence is also unobtainable should you write "only this user's observation can be confirmed, and the scope of impact cannot be verified"—do not write "there was no reset."
For external sites, prefer curl direct connections; WebFetch being blocked by a security check does not prove the site is down.feed.xml Its historical live-test performance
lags behind the API even more, so do not use it as a fallback.
When looping over multiple sites, do not reuse the same temporary file. curl -o /tmp/x.html fails (http_code=000), it neither
clears nor deletes the old file, and the next round's parsing script will print as usualthe previous site'scontent without reporting any error (2026-09-01
live test:codexreset.org retrieved 0 bytes, but the output was actually the previous round's codexrunway body text, looking completely like success).
Use a separate file name for each site, and first check http_code before parsing.
The negative assertion "he hasn't posted anything new" has a clear limit. fxtwitter only has the /status/<id> endpoint,
no user timeline(api.fxtwitter.com/<user> only returns the profile, without a tweet list), so it cannot directly
iterate through his latest tweets. Therefore "no new official announcement" can only be obtained by cross-checking aggregators (same family) + WebSearch; in essence it is "not in these
channels," not "he didn't post"—phrase it with that strength, and add that "this does not mean there was no action on the backend." When twitter-cli
is logged in, this boundary moves back:user-posts directly iterates the main post timeline, so "no new main posts within this coverage window" can be stated literally; replies
are not included in this, and cannot be announced along with it.
Tibo's time notation is sloppy (interpretation rules)
Actual quote:Reset will land around 14pm PST tomorrow.(posted 2026-08-23 06:29 UTC)
- "14pm" = 14:00 = 2 PM (he mixes 24-hour time and am/pm; just take the number literally)
- He often writes Pacific local time as "PST." US daylight saving time is the second Sunday in March to the first Sunday in November,
during which the local time is PDT (UTC-7); the primary interpretation follows the target date's
America/Los_AngelesTime zone conversion. When the original text explicitly says PST, the alternative literal UTC-8 interpretation must also be calculated, and it should be stated whether it has been clarified. The main interpretation having historical basis does not equal proof of the author's intent; a one-hour difference cannot be adjudicated as the other party being wrong based only on OS conversion. ("tomorrow" on a daylight saving time transition day would be off by 1 hour if calculated using the tweet day's offset) - "tomorrow / today" usesthe Pacific date at his tweet timeas the anchor:
announced_at(UTC) minus 7 (PDT) or 8 (PST) hours gives the Pacific date of the tweet; then read which day tomorrow refers to - "midnight" uses the same anchor (measured 2026-09-12: "a reset is also landing by midnight today" posted at 03:20 UTC = 20:20 Pacific the previous day, "today" = Pacific 9/11, i.e., before Beijing 9/12 15:00; the landing confirmation post was actually posted at Beijing 16:09)
- "by EOD" uses the same anchor and refers to Pacific midnight of that day (measured 2026-10-10: "Global reset by EOD" posted at 22:16 UTC = Pacific 15:16, main interpretation = landing before UTC next day 07:00 = Beijing next day 15:00; actual delivery was ~3h early). EOD is the upper deadline, not the expected landing time.
- Report "the main interpretation is forecast for a certain time", not "it will definitely arrive at that time". Each round, take the current time and compare it with the target; when the main interpretation has passed but completion is unconfirmed, report "the forecast time has passed, landing unverified". After both interpretations have passed, do not use time zone ambiguity to explain it as "not yet reached", and do not roll over to the next day; the official completion post time is still only the confirmation time.
- Historical pattern (not a promise): resets never land at Pacific 1AM–8AM (his sleep period); the peak is in the Pacific afternoon
Time zone conversion (commands measured, 2026-08-23;macOS only——BSD date -j/-f; GNU date has no such parameter)
# 免查时令写法(推荐):让 OS 自己解 PDT/PST。注意 macOS BSD date 的 -f 不支持
# 直接解析 "PDT" 字样(illegal time format),所以要嵌套
TZ=Asia/Shanghai date -j -r "$(TZ=America/Los_Angeles date -j -f '%Y-%m-%d %H:%M' '2026-08-23 14:00' '+%s')" '+%F %H:%M %Z'
# 输出: 2026-08-24 05:00 CST ← 太平洋夏令时下午2点 = 北京次日凌晨5点
# 已知时令时的直给写法:夏令时偏移 -0700(PDT),冬令时 -0800(PST)
TZ=Asia/Shanghai date -j -f "%Y-%m-%d %H:%M %z" "2026-08-23 14:00 -0700" "+%F %H:%M %Z"
# 反查:此刻太平洋几点(判断「tomorrow」锚哪天用)
TZ=America/Los_Angeles date "+%F %T %Z(%z)"
Evidence discipline (pitfalls already hit)
- Aggregators not recording it ≠ no reset: Radar and codexlimitwatch mainly answer "what has Tibo publicly said", not
"what happened to the backend account state". A measured counterexample on 2026-08-25: both sites stopped at 8-24, while users
from 2026-08-25 14:18 UTC onward densely posted screenshots/before-and-after values of weekly returning to 100%, and multiple people clearly stated that the original
Next resethad not yet expired or had been unexpectedly moved back, and banked remains; on the same day Tibo only officially announced restoring the Plus 5h limit. The correct conclusion is "a large-scale silent reset without official announcement was observed", not "there was no new reset", and not "a reset for everyone" without official scope proof. - First distinguish which layer the evidence proves: a product page proves one account; simultaneous-period measurements from multiple different accounts only prove "near-simultaneous cross-account observation", and each one's normal cycle is still a competing explanation; only by further proving that these accounts have not yet reached the forecast reset time does it support a common silent platform event; only official all/every wording proves scope for everyone. Write these layers into the conclusion; extrapolating globally from one screenshot is forbidden, and erasing product facts with one empty aggregator result is also forbidden.
- A tracker's confirmed label can be attached to a future forecast, and cannot be used as evidence of landing. Its API's
reset_verification_statusdoes not provide a reliable positive signal of landing. For platform completion claims, look at Tibo's follow-up original posts and their wording scope; whether a certain account has landed is judged by same-account before-and-after product readings after excluding the natural cycle and account switching. A single current balance only proves that account's current state. - In his semantics, "celebration" = the reset action itself, not posting to celebrate(2026-08-30 field lesson: reading "This celebration is moved to tomorrow as the button was already pressed today" as "just a celebration post, no reset" was disproven on the spot by two independent sources; it was fully delivered the next day—at 12:24 PT the teaser "reset will land at 6pm PST" was posted, and at 19:34 PT the confirmation "hit 25M active users…we have now reset usage for all paid subscriptions" landed). He consistently ties resets to user-milestone celebrations— the 7M/8M/20M/25M milestones were all delivered as banked/reset, at 8M his exact words were "Tomorrow might be 8M active user celebration day", and as 9M approached he launched a poll on "should we reset again" (the poll itself x.com/thsottiaux/status/2077271889626706300). So "celebration moved to tomorrow" should be read asa teaser that there will be a reset tomorrow. Calibration: this is still a hint-level official announcement (he did not write the literal "we will reset again tomorrow"), so word the conclusion as "official hint + multiple independent trackers reading it the same way = highly likely", and by convention predict it landing in the Pacific afternoon; only explicit official wording can be upgraded to "officially confirmed".
- Convert multi-source times before comparing: the time the platform posts the completion post and the time each account receives the reset may differ. Label each node with its original time zone and convert it per the "time zone conversion" section; UTC and Beijing time having the same clock-face digits does not constitute mutual proof of the same instant, nor can you directly infer that everyone was skipped from the completion post being early and individual accounts arriving late.
- Official announcement ≠ your account has received it: banked resets have a history of staggered delays, so when a user asks "why haven't I got it yet", check that account's backup reset inventory readings before and after, rather than vouching based on the announcement time.The delivery side also has observed failures: on 2026-09-09 the official side confirmed that some banked resets did not fully take effect when using ChatGPT Work/Codex, and users during the affected window were given one extra and received an apology email—when a user reports "I used a banked reset and nothing changed", first check whether it falls within that failure window.
- The "single account" premise itself must be proven first:
auth.jsononly stores the current identity and cannot rule out historical account rotation. Before drawing historical attribution conclusions, use §2 to collect identity and window clues, then name it per §3; a failed check cannot prove a single account. - Official delivery can precede the official post—for the landing moment, prefer the rollout zeroing interval or an identity-bearing API read-back; the completion post only serves as an upper bound(2026-10-07 field test:
locally in Beijing from 11:28-11:30 a 100%→0% zeroing was observed, the anchor was clean +7d, and Tibo's confirmation post only went out at 11:35, 5-7 minutes later).
The new anchor read back by query_usage
reset_atback-calculated (landing ≈ new anchor − window length) can give a narrower landing moment: 2026-10-11 Day 6 landing back-calculated to 04:00:27Z, and the confirmation post at 04:01Z was only ~33 seconds later; a read-back with email+account_ref is harder than an anonymous rollout. Order of criteria: either of the two is the best evidence for the landing moment; the completion post time only confirms "processed no later than this moment", so don't treat the confirmation post time as the arrival moment.An anchor jumping back within a few minutes after the reset lands should by default first be explained as a concurrent session lagging with the old value— the shape at the instant of landing0%→100% + 锚点回拨is a lagging session still reporting the old window from before the reset, not new evidence of account interleaving; first check whether it is right next to the landing interval, then decide whether to escalate to checking identity per §2/§3; a jump-back outside the interval is still handled per §2 ⚠️ by checking identity first.