FM-CAL-BLACKOUT: handle Chronicle calendar rebuild (V5) - #85
Open
keyxmakerx wants to merge 3 commits into
Open
Conversation
An audit of what the module ACTUALLY does when Chronicle answers 503 calendar_rebuilding confirmed half of the "degrades cleanly" claim and disproved the other half. My earlier claim that the module already handled this was only partly right, so here is the honest split. CONFIRMED SAFE (now pinned, because these are what a future edit breaks silently): a calendar 503 never aborts the rest of initial sync; no date is ever written into the Foundry world; no local Calendaria note is ever deleted; the structure-mismatch guard fails open rather than pausing; the back-catalog fires zero requests, not thirty-six; the reentrancy guard cannot leak; and the WebSocket raises no false disconnect. FIXED — the module was lying to the GM. The dashboard's bare catch turned EVERY calendar probe failure into "No calendar configured for this campaign in Chronicle", stated as fact. During the rebuild that is precisely untrue, and a 401, a proxy timeout and a DNS failure all read the same way. The classifier that gets this right already existed and had been used by the Sync Calendar editor for months; the dashboard never got it. It now has a 'rebuilding' state distinct from 'absent' (which advises importing a calendar that has nowhere to go) and 'unreachable' (which blames settings that are fine). FIXED — the push storm. Every world-time change fired two doomed requests and a red console error, un-debounced, forever; a GM running the in-game clock drove that pair per tick for the whole outage. Twenty ticks now cost ONE request and ONE notice instead of forty and twenty, via a session guard shaped after the existing real-time guard. FIXED — the shared error ring. One failing endpoint used to flush every map, actor, item and note error out of the single 50-entry log the dashboard and the diagnostics bundle both read, so an outage destroyed the diagnostics for the subsystems that still worked. Identical repeats now coalesce with a count. Also: the Pull-date button logged a successful pull that had failed; Push-date failed silently; the Overview's calendar alert keyed on a flag nothing sets, so it was dead code, and hardcoded "different structures" as the reason for any pause; and the initial-sync module loop had no per-module try/catch, unlike the post-pass loop six lines below — so every module's isolation rested on one catch inside an unrelated one. API errors now carry status, code and the server's own message, so no caller has to regex prose to find out what happened. Twelve tests, each verified failing before passing. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBbG8VuBxaTjDHEXDYMvPB
The module's docs advertised a Chronicle blocker that had been fixed 26 days earlier — the whole calendar plugin was deleted before anyone noticed. That is the failure being corrected here, not just the words. CLAUDE.md gains a Calendar blackout section (what the server answers, why 503 and not 404, what the module does about it, and that pulls, journals, maps, characters, items and notes are unaffected). The stale calendar.worldstate.changed blocker is replaced with its own history — closed by Chronicle f8d3550 on 2026-07-26, moot since the deletion — marked do-not-re-open, and the three live-client checklists that ask a tester to verify things that cannot happen are marked blocked rather than left as standing instructions. API-CONTRACT.md gains a 503 banner over the whole Calendar section (kept as the starting point for V5, explicitly NOT a description of today), a structured-errors subsection recording that this body inverts the documented field roles — the "error" field carries a machine code here, not prose — and a note that every calendar.* WebSocket type is dormant, including the consequence that a pre-blackout mismatch pause cannot be cleared by its documented recovery path until V5. README.md is the one doc a non-developer reads, so it says plainly that calendar sync is paused, that everything else still syncs, and that nothing is wrong on their side. TESTING.md stops telling a tester the calendar API returns 404 — it returns 503, and 404 is precisely the answer Chronicle avoided — and gains the three checks that ARE the blackout test. .ai.md stops carrying a third copy and points at the other two instead: three copies of one cross-repo claim is why the last one went stale unnoticed. Every cross-repo claim now carries a "Re-verify by" date, and a claim past its date is to be treated as unknown rather than as fact. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBbG8VuBxaTjDHEXDYMvPB
Closes the defence-in-depth gap booked in the previous pass. The dashboard returns before reaching classifyCalendarSyncState during the blackout, so this branch is unreachable today — which is precisely why it is worth having and pinning. Without it the classifier's fall-through answers 'date-drift' whenever a date is missing, so a Chronicle-side outage would render as "out of sync with Chronicle" and send the GM to check settings that are fine. That invariant previously held only because every caller remembered to guard it, and an invariant that depends on all its callers remembering is not one. Ranked above 'paused' deliberately: with no server-side calendar there is nothing to be paused against, and no other verdict below is computable. The dashboard is NOT given a no-op `unavailable: false` argument — the parameter already defaults to false, and passing it would imply the dashboard computes something it does not. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UBbG8VuBxaTjDHEXDYMvPB
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cites: FM-CAL-BLACKOUT (2026-08-21); CLAUDE.md → "Calendar blackout"
Security implication: none — defensive handling of a known server-side outage
Consumer-verified: Chronicle syncapi
/calendar*routes (34 endpoints) answer503 {"error":"calendar_rebuilding","message":"…"}as of 2026-08-21Foundry compatibility: verified against Foundry v12–v14 (no runtime changes)
Mockup: n/a
What this changes
Chronicle deleted its calendar plugin on 2026-08-21 for a ground-up rebuild (V5). All 34 calendar routes stay registered and answer
503 {"error":"calendar_rebuilding", …}— deliberately 503 rather than 404, so the module does not take its "that Chronicle is too old" compatibility path.This PR adds FM-CAL-BLACKOUT: a session-scoped guard that arms on the first blackout response, tells the GM exactly once, and returns before spending a request on every later push. Twenty world-time ticks now cost one request instead of forty. The api-client attaches
status,code, andserverMessageto thrown errors so callers key on structured data rather than regexing prose. Error-log repeats coalesce with a count, so one failing endpoint cannot flush the shared 50-entry ring that the dashboard and diagnostics both read. Pulls, journals, maps, characters, items and notes are unaffected — initial-sync isolation ensures one subsystem's outage cannot cost the others their sync.Why
Without this, every Foundry world-time change fires two doomed requests (the real-time pre-push probe, then the push itself) plus a red console.error — un-debounced, forever. A GM running the in-game clock or advancing time per combat round drives that pair on every tick for the whole outage, flooding the one 50-entry error ring the dashboard and diagnostics share and evicting real map/actor/item/note errors. The blackout is expected and benign (nothing is broken on the GM's side), so it should not alarm them or destroy the diagnostic surface for subsystems that still work.
Test plan
npm testpasses locally — addedtools/test-calendar-blackout.mjswith 14 tests pinning:Tenet self-check
Files changed
New:
scripts/_calendar-blackout-guard.mjs— session singleton guardtools/test-calendar-blackout.mjs— 14 regression testsModified:
scripts/_calendar-probe-state.mjs— added 'rebuilding' state classificationscripts/_realtime-date-guard.mjs— probe failure now classified instead of swallowedscripts/api-client.mjs— attach statushttps://claude.ai/code/session_01UBbG8VuBxaTjDHEXDYMvPB