fix(sync): make sync actually work — initial sync, honest badge, wire fixes (FM-SYNC-WIRE-FIX-R1)#81
Merged
Conversation
…path FM-SYNC-WIRE-FIX-R1 fix 2. onInitialSync's structure guard was gated on `_calendarModule === 'calendaria'`, so a SimpleCalendar world with a structure that differs from Chronicle's fell straight through to `_setLocalDate` and wrote Chronicle's coordinates into an incompatible calendar. Add `_readActiveSimpleCalendarStructure()` (SimpleCalendar's getCurrentCalendar() → numberOfDays/weekdays, with a getAllMonths()/getAllWeekdays() fallback) and a `_readActiveFoundryStructure()` dispatcher; the guard now covers both module paths and still fails OPEN on an unreadable structure. Committed BEFORE fix 1 (initial-sync revival) so the guard is hot in history before revived initial sync can fire date-writes into an unguarded SimpleCalendar world. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018MFs4Q928arZHLwvWWYAQm
FM-SYNC-WIRE-FIX-R1 fix 1 (FM-SYNC-1). api-client emits sync.status
UNWRAPPED — `_emit('sync.status', { status: 'connected' })` fans the object
straight to listeners — but the SyncManager listener read only
`msg.payload?.status`, so its guard was never true. `_performInitialSync`
(the full `/sync/pull` since epoch, every module's onInitialSync, the journal
duplicate-deletion post-pass) and the reconnect resync (gated on the never-set
`_initialSyncDone`) were BOTH dead on every world.
Fix the LISTENER, not the emit: extract `_onSyncStatus(msg)` reading
`msg.status ?? msg.payload?.status`. The emit stays as-is because client emit
shapes are intentionally inconsistent (sync.retryComplete uses {payload}) and
the same object feeds the wildcard router, so normalizing at the emit would
ripple. The handler is idempotent via the `_initialSyncDone` latch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MFs4Q928arZHLwvWWYAQm
…outes
FM-SYNC-WIRE-FIX-R1 fixes 3–5 + tests + docs.
Fix 3 — honest four-state calendar badge. The dashboard computed "In Sync"
from raw y/m/d equality and drove "paused" from a flag the SimpleCalendar path
never set. Replace with a pure classifier (scripts/_calendar-sync-state.mjs)
producing exactly one of: in-sync / date-drift (with direction) /
incompatible-structures (with month & weekday counts) / paused. The dashboard
feeds it the live module's pause flag AND its own structure comparison, so an
incompatibility surfaces even when the module failed OPEN. Template renders all
four states.
Fix 4 — visibility toggle. Single + bulk toggles PUT /entities/:id with only
{is_private} → UpdateEntity requires a name → 400 (swallowed). Route to
POST /entities/:id/reveal (accepts exactly {is_private}).
Fix 5 — item relations. DELETE/PUT used nested
/entities/:id/relations/:relId[/metadata] (404); create body was camelCase
with a null target. Chronicle serves flat DELETE|PUT /relations/:relationId
and binds snake_case target_entity_id/relation_type/reverse_relation_type
(required target). Fix paths + body (snake_case, object metadata); skip
relation-create when the item has no linked Chronicle entity (logged once).
Tests: tools/test-calendar-sync-state.mjs (13) + tools/test-sync-wire-fix.mjs
(16) cover fixes 1,2,4,5 and the classifier. Full suite green (704).
Docs: .ai.md decision log + Files/discrepancies/limitations; API-CONTRACT.md
erratum notes on the nonexistent nested relation paths and the reveal body;
CLAUDE.md smoke-test checklist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MFs4Q928arZHLwvWWYAQm
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: cordinator
plans/2026-07-24-calendar-remodel-requirements.md§2 ("Sync broken exactly as experienced");dispatches/foundry/FM-SYNC-WIRE-FIX-R1.mdSecurity implication: none — no new trust surface; visibility toggle now uses the correct write-permission endpoint it already had rights for.
Consumer-verified: Chronicle
syncapi/routes.go:147-148(flatPUT|DELETE /relations/:relationId),:181(POST /entities/:id/reveal);syncapi/api_handler.go:571(ToggleEntityReveal, binds{is_private}),:1219-1226(apiCreateRelationRequestsnake_case),:1249-1251(target_entity_idrequired),:1267-1268(apiUpdateRelationRequestbinds onlymetadata);internal/widgets/relations/model.go:21-31(camelCase read shape).Foundry compatibility: logic-only; exercised headlessly (
node --test). Not runtime-verified against a live client — see Coordinator must re-verify below (v12–v14 smoke-test recommended, can't be unit-tested).Mockup: n/a (badge reuses existing
.sync-badgestyles +fa-*icons).What this changes
Makes Chronicle⇄Foundry sync actually work. Five coordinator-verified defects had it reporting "syncing" while doing nothing: initial pull-sync never fired on any world, the SimpleCalendar structure guard was absent, the dashboard badge lied, the visibility toggle 400'd, and every item-relation write hit a 404 or 400. All fixes are module-side — the endpoints/behaviors they target already exist on Chronicle.
Why
plans/2026-07-24-calendar-remodel-requirements.md§2 records the operator experience ("Calendaria sync flat out doesn't work — says syncing but completely separate calendars") and the root causes; this PR is the FM-SYNC-WIRE-FIX-R1 dispatch that fixes them. Supersedes sweep BOX 3 (reports/meta/2026-07-18-audit-full-sweep-r1.md§4 box 3).Step 0 — anchors verified on current
mainEvery dispatch anchor confirmed by reading source (both repos; Chronicle read-only):
api-client.mjs:808emits_emit('sync.status', { status: 'connected' });_emit(:921-926) passes that object straight tocb(data). The listener atsync-manager.mjs:156readmsg.payload?.status. Deterministic repro (no live world needed — the guard is pure):_initialSyncDonestayedfalse, so the reconnect resync (:197, gated on_initialSyncDone) was also dead.calendar-sync.mjs:452gated the whole structure comparison on_calendarModule === 'calendaria'; the SimpleCalendar path fell through to_setLocalDate(:468) unguarded.sync-dashboard.mjs:667-669computedinSyncfrom raw y/m/d equality;structureMismatch(:675) came from a flag the SC path never set.sync-dashboard.mjs:2014+:2898PUT/entities/:idwith only{is_private}→ ChronicleUpdateEntityrequires a name → 400, swallowed.item-sync.mjs:351DELETE and:385PUT used nested/entities/:id/relations/:relId[/metadata]; create body (:306-315) was camelCase with a null target.The five fixes (file:line)
sync-manager.mjs— inline listener →_onSyncStatus(msg)readingmsg.status ?? msg.payload?.status. Fix on the LISTENER, not the emit (emit shapes are intentionally mixed —sync.retryCompleteuses{payload}— and the same object feeds the wildcard router). Revives_performInitialSyncand the reconnect resync; idempotent via_initialSyncDone.calendar-sync.mjs— new_readActiveSimpleCalendarStructure()(getCurrentCalendar()→numberOfDays/weekdays, fallbackgetAllMonths()/getAllWeekdays()) +_readActiveFoundryStructure()dispatcher; guard condition widened from Calendaria-only. Still fails OPEN on an unreadable structure. Committed before fix 1 (commit27e79f1precedes886eb24).scripts/_calendar-sync-state.mjs→classifyCalendarSyncState()yieldsin-sync/date-drift(with direction) /incompatible-structures(with counts) /paused.sync-dashboard.mjs::_buildCalendarDatafeeds it the live module's pause flag and its owncompareCalendarStructuresresult (so incompatibility shows even when the module failed OPEN).templates/sync-dashboard.hbsrenders all four./revealsync-dashboard.mjs::_onToggleVisibility+_onBulkSetVisibility→POST /entities/:id/revealwith{is_private}.item-sync.mjs— DELETE/PUT → flat/relations/:relationId; create body snake_case (target_entity_id/relation_type/reverse_relation_type) with object metadata; skips create when the item has no linked Chronicle entity (custom items have no target — logged once, no spam).⚠ Mandatory disclosures (shipping WITH them)
/sync/pullsince epoch, every module'sonInitialSync, and the journal duplicate-deletion post-pass (sync-manager.mjs:542-550). Please test on a world with existing synced data, not only a fresh world._syncChronicleEventsToCalendariaNotes(calendar-sync.mjs) is idempotent per GM (seen-set +_getLocalEventIdskip +_syncDepthecho guard), BUT mappings live in per-user GM flags (game.user.setFlag, ~:1484): a different GM account loading the world duplicates notes, and hand-authored Calendaria notes duplicate once. Documented in.ai.md; not re-architected here — flagged as a follow-up (world-scope flags).Deviations from the dispatch
metadataas a raw object, notJSON.stringify(...). Chronicle bindsmetadataasjson.RawMessage; the old stringified form stored a double-encoded JSON string. The module read path already handled both (item-sync.mjs:251), so this is a correctness improvement with no read regression. Called out per honest-deviation discipline.incompatible-structures(module NOT paused, but the dashboard reads both structures and they differ) is distinct frompaused(module stopped sync this session). This makes the badge honest in the fail-OPEN case (module skipped pausing on an unreadable structure at connect, or isn't running) rather than collapsing to three states.Test plan
node --test tools/test-*.mjs— 704 pass / 0 fail (was 675; +13test-calendar-sync-state.mjs, +16test-sync-wire-fix.mjs).node tools/check-package-descriptor.mjs— OK (0 warnings).getCurrentCalendar+ fallback + fail-open + onInitialSync pause/no-pause), fix 4 (POST/reveal, never a bare PUT), fix 5 (flat routes + snake_case body + skip-no-target), classifier (all four states reachable).Coordinator must re-verify (live client)
_performInitialSync/ "Initial sync complete"); genuine reconnect re-pulls once.Tenet self-check
/revealneeds the same write permission the old (failing) PUT did..ai.mddecision log +API-CONTRACT.mderrata updated alongside the code.Generated by Claude Code