Integrate alpha stabilization release candidate - #124
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 065da0d237
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| v_heat_delta := COALESCE((p_payload->>'heatDelta')::integer, 0); | ||
| v_morale_delta := COALESCE((p_payload->>'moraleDelta')::integer, 0); | ||
| v_pending_income_delta := COALESCE((p_payload->>'pendingIncomeDelta')::integer, 0); |
There was a problem hiding this comment.
Validate encounter rewards inside the RPC
Any authenticated player who owns a block can invoke this SECURITY DEFINER RPC directly with arbitrary integer deltas and a fresh caller-chosen result key, bypassing the Edge Function's Zod schema because the migration grants authenticated direct execution and the frontend itself uses .rpc. Repeated calls can therefore mint unbounded pendingIncome, alter morale, and create apparently valid encounter receipts/events without completing combat; verify a server-issued encounter result and enforce its permitted deltas inside the RPC, or remove direct authenticated execution.
Useful? React with 👍 / 👎.
| const { raidBlockId, clearRaid } = useRaidCheck(); | ||
| useBlockSync(!IS_DEMO_MODE); | ||
| useEmpireHydration(Boolean(authUser) && authChecked && !IS_DEMO_MODE); | ||
| useGhostCrewSync(authUser?.id ?? null, Boolean(authUser) && authChecked && !IS_DEMO_MODE); |
There was a problem hiding this comment.
Disable local ghost ticks after server hydration
On the authenticated production path this hydrates server state, but App still unconditionally starts useGhostTick() below, so every browser independently mutates those crews and claims turf every 30 seconds without writing the results back to the authoritative tables. Multiple clients consequently see different rival states, and a reload replaces the local moves and grudges with the older server snapshot; local ticking should run only in demo/offline fallback mode once authoritative hydration succeeds.
Useful? React with 👍 / 👎.
| set( | ||
| (state) => ({ | ||
| crews: Object.keys(indexedCrews).length > 0 ? indexedCrews : state.crews, | ||
| feed: feed.length > 0 ? feed.slice(0, FEED_LIMIT) : state.feed, |
There was a problem hiding this comment.
Rebuild block-store turf when hydrating crews
When an authoritative crew has non-empty ownedBlockIds, this replacement updates only the ghost store. The map, recon sheet, and attack flow read NPC territory from useBlockStore, while loadPlayerBlocks loads only the current player's blocks; unlike runTick, this path never calls buildGhostBlock/upsertBlock. A fresh authenticated device therefore cannot see or attack any durable rival turf until unrelated local ticks create new blocks, so hydration must also project or fetch the authoritative owned blocks into the block store.
Useful? React with 👍 / 👎.
| set( | ||
| (state) => ({ | ||
| crews: Object.keys(indexedCrews).length > 0 ? indexedCrews : state.crews, | ||
| feed: feed.length > 0 ? feed.slice(0, FEED_LIMIT) : state.feed, |
There was a problem hiding this comment.
Clear account-scoped feed when the server returns none
When an authenticated account has no visible world_events, this branch preserves the globally persisted local feed instead of accepting the empty authoritative result. Because slide-ghost-crews is not scoped or cleared during the account-switch reset, signing into a second account on the same browser can expose the previous account's recipient-only encounter summaries and rival activity; replace the feed even when it is empty, or scope the persisted store by profile.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Encounter RPC allows unbounded income
- Added validation bounds (±5 heat, ±100 morale, ±1M pendingIncome) to commit_encounter_result RPC to match edge function Zod schema and prevent unbounded client-supplied deltas.
- ✅ Fixed: Ghost hydration wipes local ticks
- Modified replaceAuthoritativeState to merge crews preserving local progress and disabled useGhostTick when authenticated to prevent local/server state divergence.
- ✅ Fixed: Block persist clobbers encounter effects
- Updated persistBlock to conditionally update only when lastEncounterResultKey hasn't changed, preventing stale persists from overwriting encounter-committed deltas.
Or push these changes by commenting:
@cursor push 065da0d237
You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Local ghost state shadows server hydration
- Removed timestamp comparison in replaceAuthoritativeState to always prefer server data, ensuring authenticated sessions follow the server-led world timeline.
- ✅ Fixed: Encounter key mismatch blocks persistence
- Restored appliedEncounterResultKeys from server metadata in loadPlayerBlocks, allowing persist_player_block_projection to validate encounter keys correctly.
Or push these changes by commenting:
@cursor push 60918b28a3
Preview (60918b28a3)
diff --git a/frontend/src/services/blockPersistence.service.ts b/frontend/src/services/blockPersistence.service.ts
--- a/frontend/src/services/blockPersistence.service.ts
+++ b/frontend/src/services/blockPersistence.service.ts
@@ -150,22 +150,26 @@
return [];
}
- return data.map((row: any) => ({
- id: row.id,
- address: row.address,
- owner: 'player' as const,
- heat: Math.round((row.block_heat ?? 0) / 20),
- incomePerTick: row.base_income ?? 0,
- morale: row.metadata?.morale ?? 80,
- pendingIncome: row.metadata?.pendingIncome ?? 0,
- viewMode: row.metadata?.viewMode ?? 'topdown',
- streetBackdropUrl: row.metadata?.streetBackdropUrl,
- topdownBgUrl: row.metadata?.topdownBgUrl,
- dnaId: row.metadata?.dnaId,
- incomeMultiplier: row.metadata?.incomeMultiplier,
- heatDecayMultiplier: row.metadata?.heatDecayMultiplier,
- maxMembers: row.metadata?.maxMembers,
- }));
+ return data.map((row: any) => {
+ const lastEncounterResultKey = row.metadata?.lastEncounterResultKey;
+ return {
+ id: row.id,
+ address: row.address,
+ owner: 'player' as const,
+ heat: Math.round((row.block_heat ?? 0) / 20),
+ incomePerTick: row.base_income ?? 0,
+ morale: row.metadata?.morale ?? 80,
+ pendingIncome: row.metadata?.pendingIncome ?? 0,
+ viewMode: row.metadata?.viewMode ?? 'topdown',
+ streetBackdropUrl: row.metadata?.streetBackdropUrl,
+ topdownBgUrl: row.metadata?.topdownBgUrl,
+ dnaId: row.metadata?.dnaId,
+ incomeMultiplier: row.metadata?.incomeMultiplier,
+ heatDecayMultiplier: row.metadata?.heatDecayMultiplier,
+ maxMembers: row.metadata?.maxMembers,
+ appliedEncounterResultKeys: lastEncounterResultKey ? [lastEncounterResultKey] : undefined,
+ };
+ });
}
// ─── Placement CRUD ──────────────────────────────────────────
diff --git a/frontend/src/stores/ghostCrewStore.ts b/frontend/src/stores/ghostCrewStore.ts
--- a/frontend/src/stores/ghostCrewStore.ts
+++ b/frontend/src/stores/ghostCrewStore.ts
@@ -195,13 +195,10 @@
const nextCrews = { ...state.crews };
for (const remoteCrew of crews) {
- const localCrew = state.crews[remoteCrew.id];
- // Browser state may have progressed while the initial fetch was
- // in flight. Keep the newest complete crew record; authenticated
- // sessions disable local ticking once hydration is active.
- const localTick = localCrew ? Date.parse(localCrew.lastTickAt) : Number.NEGATIVE_INFINITY;
- const remoteTick = Date.parse(remoteCrew.lastTickAt);
- nextCrews[remoteCrew.id] = localCrew && localTick > remoteTick ? localCrew : remoteCrew;
+ // In authenticated sessions, the server is authoritative.
+ // Always overlay remote crew data to ensure production sessions
+ // follow the server-led world timeline rather than stale local seeds.
+ nextCrews[remoteCrew.id] = remoteCrew;
}
const seen = new Set<string>();You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 8765f9a. Configure here.
| return true; | ||
| }).slice(0, FEED_LIMIT); | ||
|
|
||
| return { crews: nextCrews, feed: mergedFeed }; |
There was a problem hiding this comment.
Local ghost state shadows server hydration
High Severity
replaceAuthoritativeState keeps a local crew whenever its lastTickAt is newer than the server snapshot. Seeded and persisted local timestamps are almost always later than the static database seed, so authenticated hydration never overlays durable Ghost Crew state.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8765f9a. Configure here.
| if (!projectionError) { | ||
| if (data?.applied === false) { | ||
| console.warn('[BlockPersistence] Skipped stale block projection after a newer encounter result.'); | ||
| return; |
There was a problem hiding this comment.
Encounter key mismatch blocks persistence
High Severity
The new projection RPC rejects writes when p_client_result_key does not match lastEncounterResultKey, while persistBlock only sends the in-memory appliedEncounterResultKeys tail and returns without retry on applied: false. After an encounter those keys can diverge, and later block autosaves never persist.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8765f9a. Configure here.



Purpose
This is the single integration path for the three reviewed, non-overlapping alpha-stabilization pull requests. It was assembled in an isolated worktree and is intentionally not merged into
main-tL2525.Integrated source work
All three heads merged locally without textual conflicts. The integration adds no new product scope beyond those source pull requests.
Combined validation
npm run validate— 50 test files / 716 tests passed; existing lint warnings remained warnings only; asset audit passed with zero errors; five packages validated.VITE_DEMO_MODE=1production build — passed.pytest -q— 42 tests passed.Guardrails
005_authoritative_world_foundation.sqlis not applied by this PR; database and Edge Function deployment remain separate, reviewed release gates.Merge policy
If this PR is approved and all checks pass, merge this integration PR only. Then close #121, #122, and #123 as superseded. Do not merge the three source PRs individually afterwards.
See
docs/INTEGRATION_ALPHA_STABILIZATION_REPORT.mdfor evidence and release gates.Note
Medium Risk
Adds security-definer RPCs, block metadata/heat updates from encounter receipts, and cron-gated world ticks; changes are additive but affect persistence reconciliation and require correct migration/secret deployment before production.
Overview
This PR is the single merge path for three reviewed scopes: a CC0 production character/PBR art package (#121), non-demo startup recovery when Supabase/env is missing (#122), and an additive authoritative Ghost Crew / encounter-receipt backend with matching client hooks (#123).
Art (#121): Docs and
ASSETS.mdnow record shipped Quaternius CC0 base + animation clips asuniversal-male.v1.glbwith license/manifest; the Modern Ops path loads the strict package before scene readiness and falls back on failure (per integration validation).World (#123): New migrations add
ghost_crews,world_ticks,world_events,encounter_results, andclaimed_block_dna, plus idempotent RPCsapply_ghost_world_tick(service-only) andcommit_encounter_result(authenticated, block-owner). Migration 006 bounds encounter deltas and addspersist_player_block_projectionso debounced block sync cannot overwrite a newer encounter receipt. Edge functionscombat/commit-resultandworld-tick-ghost(cron secret) call those RPCs. The app hydrates ghost crews viauseGhostCrewSync, disables local browser ghost ticks when authenticated, posts completed encounters fromBlockModeView, and routes UUID block saves through the atomic projection RPC.Process:
docs/INTEGRATION_ALPHA_STABILIZATION_REPORT.mddocuments combined test/build gates; DB and edge deployment remain out-of-band until reviewed.Reviewed by Cursor Bugbot for commit 8765f9a. Bugbot is set up for automated code reviews on this repo. Configure here.