시간 도메인과 정지 중 메시지·베팅 경계 정리

This commit is contained in:
2026-09-03 16:01:19 +00:00
parent 10cddbb565
commit abceee8315
108 changed files with 3504 additions and 1473 deletions
+103 -12
View File
@@ -16,13 +16,23 @@
"world_state.deadline_generation",
"general.turn_tick",
"general.recent_war_tick",
"general.meta.next_change_tick",
"select_pool.reserved_until_tick",
"select_npc_token.valid_until_tick",
"select_npc_token.pick_more_from_tick",
"message.time_tick",
"message.valid_until_tick",
"message.occurred_game_tick",
"message_action.created_game_tick",
"message_action.expires_game_tick",
"message_action.resolved_game_tick",
"message_action.clock_revision",
"message_action.deadline_generation",
"inheritance_ledger.applied_clock_revision",
"inheritance_ledger.applied_deadline_generation",
"auction.open_tick",
"auction.close_tick",
"auction_bid.occurred_game_tick",
"vote_poll.start_tick",
"vote_poll.end_tick",
"clock_suspension.source_revision",
@@ -34,6 +44,43 @@
"clock_suspension.aligned_tick",
"clock_projection_outbox.target_revision"
],
"wallTimeFields": [
"input_event.created_at",
"input_event.processing_at",
"input_event.completed_at",
"input_event.lease_until",
"read_model_outbox.available_at",
"read_model_outbox.locked_at",
"read_model_outbox.delivered_at",
"web_push_outbox.available_at",
"web_push_outbox.locked_at",
"web_push_outbox.delivered_at",
"turn_daemon_lease.lease_until",
"turn_daemon_lease.heartbeat_at",
"message.created_at_wall",
"message.delete_until_wall",
"message.tombstoned_at_wall",
"message_read_state.updated_at",
"diplomacy_letter.date",
"auction_bid.requested_at_wall",
"auction_bid.created_at",
"auction.finalizing_at",
"auction.finished_at",
"inheritance_ledger.requested_at_wall",
"inheritance_ledger.consumed_at_wall",
"inheritance_ledger.created_at_wall",
"clock_suspension.cut_wall_at",
"clock_suspension.resume_wall_at"
],
"excludedFromReconciliation": [
"all WALL_TIME created_at and updated_at audit fields",
"normal message envelope and five-minute deletion lifecycle",
"account and inheritance receipt timestamps",
"traffic and general-access periods",
"notification and outbox delivery/retry timestamps",
"daemon, worker, editor, gateway, and release leases",
"board, authentication, account, audit, and operator timestamps"
],
"participants": [
{
"key": "world-clock",
@@ -56,6 +103,13 @@
"projectionFields": ["general.turn_time"],
"owner": "game-engine/turn-daemon"
},
{
"key": "selection-reselection-deadline",
"policy": "SHIFT",
"authorityFields": ["general.meta.next_change_tick"],
"projectionFields": ["general.meta.next_change", "general.meta.nextChangeAt"],
"owner": "game-engine/select-pool"
},
{
"key": "general-recent-war-occurrence",
"policy": "KEEP",
@@ -67,8 +121,15 @@
"key": "auction-open-occurrence",
"policy": "KEEP",
"authorityFields": ["auction.open_tick"],
"projectionFields": ["auction.created_at"],
"owner": "game-api/auction"
"projectionFields": [],
"owner": "game-engine/auction"
},
{
"key": "auction-bid-occurrence",
"policy": "KEEP",
"authorityFields": ["auction_bid.occurred_game_tick"],
"projectionFields": ["auction_bid.event_at"],
"owner": "game-engine/auction"
},
{
"key": "auction-deadline",
@@ -85,18 +146,37 @@
"owner": "game-api/auction-worker"
},
{
"key": "message-occurrence",
"key": "message-action-occurrence",
"policy": "KEEP",
"authorityFields": ["message.time_tick"],
"projectionFields": ["message.time"],
"owner": "game-engine/message"
"authorityFields": ["message_action.created_game_tick"],
"projectionFields": ["message.time", "message.time_tick"],
"rowScope": "messages with a message_action row only",
"owner": "game-engine/message-action"
},
{
"key": "message-expiry",
"key": "message-action-expiry",
"policy": "SHIFT",
"authorityFields": ["message.valid_until_tick"],
"projectionFields": ["message.valid_until"],
"owner": "game-engine/message"
"authorityFields": ["message_action.expires_game_tick"],
"projectionFields": ["message.valid_until", "message.valid_until_tick"],
"rowScope": "messages with a message_action row only",
"owner": "game-engine/message-action"
},
{
"key": "message-action-clock-coordinate",
"policy": "REBUILD",
"authorityFields": ["message_action.clock_revision", "message_action.deadline_generation"],
"projectionFields": [],
"owner": "game-engine/message-action"
},
{
"key": "inheritance-effect-coordinate",
"policy": "KEEP",
"authorityFields": [
"inheritance_ledger.applied_clock_revision",
"inheritance_ledger.applied_deadline_generation"
],
"projectionFields": [],
"owner": "game-engine/inheritance"
},
{
"key": "vote-start-occurrence",
@@ -127,7 +207,7 @@
"owner": "game-engine/npc-selection"
},
{
"key": "accepted-command-coordinate",
"key": "daemon-command-coordinate",
"policy": "KEEP",
"authorityFields": [
"input_event.accepted_game_tick",
@@ -139,7 +219,7 @@
"input_event.processing_clock_revision",
"input_event.processing_deadline_generation"
],
"owner": "game-api/input-event"
"owner": "game-engine/input-event-claim"
},
{
"key": "tournament-deadlines",
@@ -220,6 +300,17 @@
"clock_projection_outbox.available_at",
"clock_projection_outbox.locked_at",
"clock_projection_outbox.applied_at",
"message.created_at_wall",
"message.delete_until_wall",
"message.tombstoned_at_wall",
"message_action.created_at_wall",
"message_action.updated_at_wall",
"auction.created_at",
"auction.updated_at",
"auction_bid.requested_at_wall",
"auction_bid.created_at",
"inheritance_ledger.requested_at_wall",
"inheritance_ledger.consumed_at_wall",
"*.created_at",
"*.updated_at"
]
+33 -10
View File
@@ -3,11 +3,13 @@
## Product contract
Gameplay time is an integer `GameTick`; one turn is permanently `36,000,000`
ticks. Wall time is an observation and operational-control input, never the
authority for gameplay ordering. A long suspension advances the observed game
coordinate to the resume wall instant without replaying skipped turns, monthly
events, RNG, auctions, or tournaments. Every movable future schedule is shifted
by the same exact tick delta, including the sub-turn remainder.
ticks. Wall time is separately authoritative for account, community, audit,
lease, retry, notification, and operational rules. It is never projected into a
game deadline. A long suspension advances the observed game coordinate to the
resume wall instant without replaying skipped turns, monthly events, RNG,
auctions, or tournaments. Every movable future GAME schedule is shifted by the
same exact tick delta, including the sub-turn remainder. WALL occurrences and
deadlines are outside that operation.
The clock state is stored in `world_state`:
@@ -50,6 +52,16 @@ The authoritative registry is
architecture gate rejects a new tick/revision field that is absent from that
inventory.
The participant set contains only GAME authority or its projections: world and
turn cursors, general turns/recent-war occurrences/reselection deadlines,
auction occurrences/deadlines, actionable-message occurrences/deadlines,
vote deadlines, selection/NPC windows, input-event game coordinates,
tournament Redis deadlines, and clock-operation metadata. A normal message's
`created_at_wall`/`delete_until_wall`, inheritance receipts, notification and
outbox retry timestamps, leases, and audit columns are explicitly excluded.
The former broad `message-expiry` meaning is split into
`message-action-expiry`; an envelope has no GAME lifetime.
## Unification wait
A unification month with an invader choice changes `RUNNING -> SUSPENDED` and
@@ -109,9 +121,11 @@ turn-daemon fencing row
-> Redis outbox projection
```
The ordinary turn flush already validates phase, revision, and deadline
generation after taking this lock prefix. Clock operation participants will be
added without changing that prefix.
The ordinary turn flush and daemon command claim validate phase, revision, and
deadline generation after taking this lock prefix. WALL-only message/account
operations do not take this lock and remain available while suspended. Hybrid
operations commit their GAME effect only behind this fence; inheritance debit,
receipt, effect, and command success are one transaction.
## Opening invariant
@@ -137,8 +151,17 @@ all present. Before that boundary, the loader and ordinary turn-flush fence both
treat the row as legacy `MANUAL`; the first fenced flush installs the complete
snapshot atomically instead of trusting the new column's `RUNNING` database
default. Input-event acceptance does not use that compatibility fallback: an
API or worker may enqueue gameplay only after the authoritative clock is fully
initialized.
API or worker records only a DB-wall receipt, then the daemon establishes the
GAME coordinate while claiming under the authoritative fence. Rolling-upgrade
payload coordinates may be parsed and ignored, but never become rule authority.
Migration `20260903140000_split_message_wall_and_game_time` separates message
envelopes from actions and adds explicit auction-bid occurrence/request facts,
inheritance receipts, and selection cooldown tick authority. Legacy projection
columns remain temporarily for old readers. A missing GAME tick fails closed;
it never changes the rule to WALL_TIME. A WALL rule likewise never derives an
authority tick. See [`time-domains.md`](./time-domains.md) for the complete
inventory and migration policy.
No active participant remains `FORBID`. Tournament writes carry
tick/revision/generation coordinates and are revision-fenced in Redis.
+36 -18
View File
@@ -1,17 +1,20 @@
# 게임 시계
게임 진행 시각은 `world_state.clock_tick`이 기준입니다. 벽시계는 daemon lease,
요청 timeout, 처리 budget과 같은 운영 제어에만 사용합니다. 장수 턴, 메시지
유효기간, 투표, 경매와 대회 마감은 game tick 또는 그 tick에서 투영한 시각을
사용합니다.
시간 규칙은 `GAME_TIME`, `WALL_TIME`, `MONOTONIC_ELAPSED_TIME`
나뉩니다. 게임 진행의 권위는 `world_state.clock_tick`, 영속 wall
판정의 권위는 PostgreSQL UTC 시계, 프로세스 내부 경과시간의 권위는
monotonic clock입니다. 장수 턴·외교 효력·게임 경매·투표·대회는 GAME,
일반 메시지·계정·감사·lease·retry는 WALL입니다. 전체 필드별 계약은
[`time-domains.md`](./time-domains.md)를 따릅니다.
한 턴은 항상 `36,000,000` tick입니다. `tick_seconds`가 바뀌면 현재 표시
한 턴은 항상 `36,000,000` tick입니다. `tick_seconds`가 바뀌면 현재 GAME 표시
시각이 유지되도록 `clock_base_time`을 다시 계산하므로, 기존 장수 턴 순서와
남은 턴 수가 보존됩니다. DateTime 필드는 이전 데이터와 화면을 위한 투영값이며
tick 필드가 존재하면 tick이 우선합니다.
남은 턴 수가 보존됩니다. GAME 규칙의 DateTime은 화면/레거시 투영일 뿐이며
tick이 반드시 authority입니다. WALL 규칙은 tick이 없어도 정상이며
DateTime을 tick으로 변환해 판정하지 않습니다.
운영 중 턴 간격 변경은 Gateway의 내구성 런타임 작업으로만 수행합니다. 같은
transaction에서 `world_state`, 장수·경매·메시지·설문 투영값과 checkpoint를
transaction에서 `world_state`, 장수·경매·actionable message·설문 투영값과 checkpoint를
갱신하며 기존 역사/행동 로그의 `created_at`은 다시 쓰지 않습니다. 토너먼트의
Redis 투영은 DB commit 뒤 action ID로 멱등 적용됩니다.
@@ -29,6 +32,18 @@ Redis 투영은 DB commit 뒤 action ID로 멱등 적용됩니다.
프로필 설치 시 선택한 모드는 DB에 저장됩니다. daemon의 환경변수는 로드한
모드를 명시적으로 덮어쓸 때만 사용해 주세요.
`SUSPENDED``RECONCILING`에서는 `GameClock.nowTick()`이 wall anchor 이후의
현실 경과시간을 더하지 않고 저장된 `clock_tick`을 그대로 반환합니다. 따라서
24시간 동안 정지해도 actionable message, 토너먼트와 국가 베팅의 GAME deadline은
줄지 않습니다. 일반 메시지 envelope와 5분 삭제 기한은 별도의 DB WALL_TIME이라
같은 기간 계속 흐릅니다. 정지 전에 도착한 등용장도 envelope로 계속 수신·열람할
수 있지만, 등용 수락 효과는 daemon GAME fence가 다시 열릴 때까지 적용되지 않습니다.
이미 열린 토너먼트·국가 베팅에는 `SUSPENDED` 중에도 새 베팅을 제출할 수 있습니다.
이때 베팅 가능 여부는 frozen GAME coordinate로 판정하고, 재화 mutation은 현재
phase/revision/generation을 다시 잠가 검증합니다. 단계 전환·마감·정산은 실행하지
않으며, 원자적 reconciliation이 진행되는 `RECONCILING`에서는 새 베팅도 받지 않습니다.
## 중단 후 재개
realtime daemon은 재개할 때 Ref `checkDelay()`와 같은 한도를 적용합니다.
@@ -45,21 +60,24 @@ realtime daemon은 재개할 때 Ref `checkDelay()`와 같은 한도를 적용
장수 턴 tick은 바꾸지 않습니다. 동시에 `clock_wall_anchor`를 작업 실행
시각으로 다시 고정합니다.
DB migration은 기존 DateTime 에서 tick을 채웁니다. 새 설치와 migration
DB migration은 GAME 규칙의 기존 DateTime 투영에서 tick을 채웁니다. 새 설치와 migration
재실행은 `prisma:migrate:deploy:game`으로 수행합니다. 메시지의 연도 9999 같은
무기한 호환값은 안전한 정수 범위를 넘을 수 있으므로 tick을 `NULL`로 두고
DateTime fallback을 사용합니다.
무기한 호환값은 일반 메시지의 투영일 뿐입니다. actionable deadline은
`expires_game_tick`, 일반 삭제 deadline은 `delete_until_wall`만이 authority이며
NULL에 따라 다른 시계로 fallback하지 않습니다.
## 비동기 작업의 시계 경계
게임 규칙의 수락·입찰·예약 시각은 logical game time을 사용하지만 daemon
queue의 `InputEvent.createdAt`, worker history retention과 timeout은 운영
벽시계를 사용합니다. NPC 빙의 enqueue는 현재 logical game time을 event
payload의 `acceptedGameAt`에 고정합니다. queue에 들어갈 때 유효했던 token은
처리 전 game tick이 진행해도 이 저장된 논리 수락 시각으로 다시 검증합니다.
외부 요청은 `InputEvent.createdAt` DB WALL_TIME으로 접수합니다. API payload가
game tick을 미리 고정하지 않으며, daemon이 clock lock/fence 아래서 claim할 때
`accepted_game_tick`/세대와 `processing_game_tick`/세대를 확정합니다. NPC,
선택, 투표, 경매, 유산 효과는 처리 tick으로 검증하며 stale revision은
적용하지 않습니다. worker history retention·lease·retry는 DB WALL_TIME,
프로세스 대기 budget은 monotonic time입니다.
경매 입찰은 같은 logical tick에서 여러 번 일어날 수 있습니다. bid 표시
시각은 같은 game time을 보존하고, optimistic 경합 판정은 임의 UUID의
경매 입찰은 `requested_at_wall`로 현실 요청을, `occurred_game_tick`으로 GAME
사건을 별도 기록합니다. bid 표시 투영은 같은 game time을 보존하고,
optimistic 경합 판정은 임의 UUID의
사전순이 아니라 읽은 `latest_event_id`를 버전 토큰으로 사용합니다. worker
재시작 시 `OPEN``close_tick` deadline에, 이미 마감 판정이 끝난
`FINALIZING`은 현재 tick에 seed하여 durable finalization event 복구를 즉시
+166
View File
@@ -0,0 +1,166 @@
# Time-domain inventory
This document is the authoritative classification of persistent timestamps,
deadlines, cooldowns, and process-local elapsed-time rules. Classification is
per rule, not per table. A feature may record both a wall occurrence and a game
effect; those are two facts, never one fallback clock.
## Domain contract
| Domain | Authority | Advances while suspended/reconciling | Reconciliation |
| ------------------------ | -------------------------------------------------------------- | ------------------------------------ | ----------------------------- |
| `GAME_TIME` | `world_state.clock_tick` under phase/revision/generation fence | no | `SHIFT`, `KEEP`, or `REBUILD` |
| `WALL_TIME` | PostgreSQL UTC `CURRENT_TIMESTAMP` for persistent decisions | yes | never |
| `MONOTONIC_ELAPSED_TIME` | `performance.now()` / monotonic process clock | process-local only | never persisted |
`GameTick`, `ClockRevision`, `DeadlineGeneration`, `WallInstant`, and
`MonotonicDuration` name these meanings in new/refactored APIs. Existing
`createdAt`/`updatedAt` fields remain wall audit timestamps unless this inventory
explicitly calls them game projections.
## Game database inventory
`Pause` means whether the rule continues to age during `SUSPENDED` or
`RECONCILING`. `Projection` means a non-authoritative compatibility/display
representation.
| Table / rule / field(s) | Current meaning | Domain and authority | Pause | Reconcile / projection | Decision and reason |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `world_state.clock_tick` | observed world coordinate | GAME, self-authoritative | stop | REBUILD | root of all game-time decisions |
| `world_state.last_turn_tick` | executed turn cursor | GAME | stop | SHIFT | execution order must preserve skipped-turn policy |
| `world_state.clock_revision`, `deadline_generation` | clock/future-deadline generations | GAME metadata | stop | REBUILD | stale commands/workers must fail their fence |
| `world_state.clock_base_time`, `clock_wall_anchor` | tick-to-date mapping and wall observation anchor | GAME projection metadata | n/a | REBUILD | not business wall deadlines |
| `world_state.updated_at` | row audit | WALL, DB UTC | advance | excluded | operational history is not shifted |
| `clock_suspension.source_revision`, `target_revision`, `cut_tick`, `catch_up_ticks`, `gap_ticks`, `shift_ticks`, `aligned_tick` | reconciliation plan/audit | GAME metadata | stop | KEEP | immutable clock operation facts |
| `clock_suspension.cut_wall_at`, `resume_wall_at`, `created_at`, `updated_at` | operator/runtime occurrence audit | WALL, DB UTC | advance | excluded | records when the real operation occurred |
| `clock_projection_outbox.target_revision` | target GAME generation | GAME metadata | stop | KEEP | projection fence |
| `clock_projection_outbox.available_at`, `locked_at`, `applied_at`, `created_at`, `updated_at` | retry/lease/audit | WALL, DB UTC | advance | excluded | worker control cannot pause with game time |
| `clock_reconciliation_participant` checksum/count/policy | immutable operation evidence | GAME operation metadata | n/a | KEEP | evidence, not a deadline |
| `input_event.accepted_game_tick`, `accepted_clock_revision`, `accepted_deadline_generation` | daemon-claim boundary | GAME metadata | stop | KEEP across matching revision; rebase pending legacy rows only | API does not pre-stamp these; daemon owns acceptance |
| `input_event.processing_game_tick`, `processing_clock_revision`, `processing_deadline_generation` | actual mutation boundary | GAME metadata | stop | KEEP | effect validation/RNG uses this coordinate |
| `input_event.created_at`, `processing_at`, `completed_at`, `lease_until` | request receipt, processing audit, lease | WALL, DB UTC | advance | excluded | external occurrence and worker lease |
| `read_model_outbox.*_at`, `web_push_outbox.*_at` | availability, claim, delivery, audit | WALL, DB UTC | advance | excluded | retry and notification delivery are operational |
| `turn_daemon_lease.lease_until`, `heartbeat_at` | daemon liveness | WALL, DB UTC | advance | excluded | a paused game must still lose a dead daemon lease |
| `general.turn_tick` | next general turn | GAME | stop | SHIFT; `turn_time` projection | determines engine order |
| `general.recent_war_tick` | past battle occurrence | GAME | stop | KEEP; `recent_war_time` projection | historical event does not move |
| `general.meta.next_change_tick` | N-turn reselection cooldown | GAME | stop | SHIFT; `next_change`/`nextChangeAt` projections | expressed in turns; missing tick fails closed |
| `general.created_at`, `updated_at` | entity audit | WALL, DB UTC | advance | excluded | no gameplay deadline meaning |
| `select_pool.reserved_until_tick` | selection reservation deadline | GAME | stop | SHIFT; `reserved_until` projection | reservation is measured in game turns |
| `select_npc_token.valid_until_tick`, `pick_more_from_tick` | NPC selection windows | GAME | stop | SHIFT; DateTime projections | token is a game selection schedule; missing ticks fail closed |
| `general_access_log.last_refresh`, `last_action_at`; `general_access_batch.created_at`; `traffic_period.started_at`, `last_refresh`; `traffic_period_general.last_refresh` | traffic/access accounting | WALL, DB UTC | advance | excluded | community/operations usage, not world progression |
| `message.created_at_wall`, `delete_until_wall`, `tombstoned_at_wall` | envelope send/delete lifecycle | WALL, DB UTC | advance | excluded | normal messages work while the game is paused; deletion is real five minutes |
| `message.occurred_game_tick` | optional game context | GAME occurrence | stop | KEEP | context only, never deletion authority |
| `message.time`, `time_tick`, `valid_until`, `valid_until_tick` | rolling compatibility projections | projection only | n/a | recompute only for `message_action`; general-envelope values never decide lifecycle | old columns are not fallback authority |
| `message_action.created_game_tick`, `resolved_game_tick` | action occurrence/resolution | GAME occurrence | stop | KEEP | actionable message lifecycle is separate from envelope |
| `message_action.expires_game_tick` | proposal response deadline | GAME | stop | SHIFT | remaining game duration survives pause |
| `message_action.clock_revision`, `deadline_generation` | response fence | GAME metadata | stop | REBUILD | stale responses are rejected |
| `message_action.created_at_wall`, `updated_at_wall`; `message_read_state.updated_at` | audit/read occurrence | WALL, DB UTC | advance | excluded | community UX state |
| `diplomacy_letter.date` | document authored/sent time | WALL, DB UTC | advance | excluded | game effect dates live in diplomacy/action state, not the document timestamp |
| diplomacy war/nonaggression start/end month data | diplomatic effect schedule | GAME calendar | stop | handled by engine schedule | affects world turns and war validity |
| `inheritance_point.updated_at`, `inheritance_log.created_at`, `inheritance_result.created_at`, baseline/user-state audit fields | account ledger/result audit | WALL, DB UTC | advance | excluded | account/external-currency history |
| `inheritance_ledger.requested_at_wall`, `consumed_at_wall`, `created_at_wall` | direct purchase receipt | WALL, DB UTC | advance | excluded | real request/debit receipt |
| `inheritance_ledger.applied_clock_revision`, `applied_deadline_generation` | game-effect fence metadata | GAME metadata | stop | KEEP | no `applied_game_tick`: current direct effects are timeless immediate state changes |
| inheritance command `input_event` | durable effect state/idempotency | WALL receipt + GAME processing fence | mixed, separated | only GAME coordinate participates | one transaction commits debit, receipt, effect, and command success; failure leaves durable input event and no debit |
| `auction.open_tick`, `auction_bid.occurred_game_tick` | open/bid game occurrence | GAME | stop | KEEP; bid `event_at` is projection | event order/RNG/replay context |
| `auction.close_tick` | in-world close deadline | GAME | stop | SHIFT; `close_at` projection | authoritative worker/finalizer deadline; missing tick fails closed |
| `auction_bid.requested_at_wall`, `created_at`; `auction.finalizing_at`, `finished_at`, `created_at`, `updated_at` | request/processing/audit | WALL, DB UTC | advance | excluded | real action and recovery history |
| `auction.latest_event_at` | optimistic compatibility projection of latest game event | GAME projection | stop | follows authoritative event tick | never used as wall deadline |
| `vote_poll.start_tick`, `end_tick` | poll occurrence/deadline | GAME | stop | KEEP/SHIFT; `start_at`/`end_at` projections | poll is an in-world survey; missing deadline tick fails closed |
| `vote_poll.closed_at`, `created_at`, `updated_at`; `vote.created_at`; `vote_comment.created_at` | closure/user/audit occurrence | WALL, DB UTC | advance | excluded | closure receipt and community content history |
| tournament `nextTick`, `bettingCloseTick` in Redis | stage/betting deadlines | GAME | stop | REBUILD; `nextAt`/`bettingCloseAt` projections | stages advance with the world; legacy date-only state fails closed |
| nation betting open/close year-month and tournament phase | in-world availability | GAME calendar/tick | stop | engine/Redis participant | tied to tournament turns |
| tournament/nation bet submission | user WALL request + effect at the current frozen GAME coordinate | WALL + GAME, separated | submission is allowed during `SUSPENDED`; GAME deadline does not age | receipt excluded; GAME availability/fence retained | pausing stage progress must not close an already-open betting window |
| `nation_betting.*_at`, `nation_bet.*_at` | user/audit occurrence | WALL, DB UTC | advance | excluded | receipts, not close authority |
| `game_history.date`, old-general `turntime`, archived projected dates | archived game-calendar projection | GAME historical display | stop | KEEP, never shifted | immutable archive/replay record |
| archive/entity `created_at`, cancellation `opened_at`/`cancelled_at`, unification `completed_at` | operation/archive audit | WALL, DB UTC | advance | excluded | real creation/completion facts |
| `general_turn_revision.lease_expires_at`, `nation_turn_revision.lease_expires_at` and audit timestamps | edit lease/revision audit | WALL, DB UTC | advance | excluded | editor concurrency timeout |
| board post/comment, log/error/event, legacy migration timestamps | content/audit/migration history | WALL, DB UTC | advance | excluded | community and operational evidence |
The year-9999 message sentinel remains a legacy projection only.
`MAX_SAFE_GAME_TICK` is the separate GAME-domain infinite sentinel. Neither is
converted into or used as the other domain's ordinary deadline.
## Gateway database inventory
Gateway has no gameplay clock authority. Every Gateway `DateTime` is WALL_TIME:
- `app_user`: identity/session/icon/terms/privacy/Kakao/grace/deletion/login and
`created_at`/`updated_at` fields.
- access grants, retired identities, admin audits, user icons, legacy member
logs, and migration timestamps.
- profile lifecycle `preopen_at`, `open_at`, `scheduled_start_at`, build request,
start/completion/last-used, and row audit timestamps. These are real control
plane schedules; they do not replace a profile's `world_state.clock_tick`.
- subscriptions/preferences/receipts/notifications and web-push delivery
`available_at`, `locked_at`, `delivered_at`, expiration and audit fields.
- runtime actions, operations, releases, and bulk releases: schedule, start,
completion, retry, lease, heartbeat, successful and audit timestamps.
Competitive Gateway operation/release claim and lease renewal decisions read
PostgreSQL `CURRENT_TIMESTAMP` inside the persistence transaction; the caller's
poll timestamp is not authoritative. A Gateway PREOPEN wall schedule is an operational request; once
the game exists, gameplay schedules use the game database tick.
## Process-local monotonic inventory
The following are `MONOTONIC_ELAPSED_TIME` and are never persisted: daemon/RPC
wait budgets, turn processing budgets, worker poll/resync intervals, lock
acquisition waits, readiness loops, short-lived cache TTLs, latency metrics, and
test wait loops. Production implementations use `performance.now()` where an
elapsed duration is measured. `Date.now()`/`new Date()` remains valid only when
creating or formatting a WALL occurrence, calculating a non-competitive auth
TTL for an external protocol, or providing an explicit test clock.
## API phase policy
| Operation | During `SUSPENDED` / `RECONCILING` | Fence |
| -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| normal public/private/nation message send/read/delete, including receiving/reading an existing recruitment letter envelope | allowed | WALL DB transaction only; actionable deadline remains frozen GAME state |
| notification/account/inheritance history/audit reads | allowed | WALL |
| actionable message response | rejected/queued except the explicitly authorized unification response | daemon clock phase/revision/generation |
| tournament/nation bet submission while its GAME window is open | allowed in `SUSPENDED`; rejected in `RECONCILING` | frozen GAME deadline + phase/revision/generation fence |
| tournament stage transition/close/settlement and nation-bet close/settlement | not applied | daemon GAME fence |
| turn, reservation, war/diplomacy effect, auction, vote, other tournament mutation | not applied | daemon GAME fence |
| direct inheritance state mutation | rejected if the daemon GAME fence cannot commit | atomic input-event + revision/generation fence |
There is currently no account-only inheritance purchase endpoint. Direct
inheritance commands use policy 1 (no debit if game mutation cannot commit), not
an ambiguous partially-applied state. Their `InputEvent.requestId` is the durable
idempotency/effect state; `InheritanceLedger.requestId` proves the one successful
receipt.
## `loadCurrentGameTime` call-site audit
All production call sites were reviewed. The remaining uses are GAME-only:
- `messages/store`: create/read/invalidate the separate `message_action`; normal
envelope creation, display, read state, and deletion do not load game time.
- `messages/diplomaticResponse`: game-effect/log calendar context after an
actionable response fence; the letter's authored date is DB WALL_TIME.
- auction `open`, `scheduler`, `worker`, and router: open/close tick projection,
due evaluation, and fence context; bid receipt time is separate WALL_TIME.
- vote router, tournament router/worker: GAME poll/stage deadlines and Redis
projection fences.
- troop/general/join selection routers: current world turn/schedule context;
the NPC reservation mutation holds the clock advisory lock and `world_state`
row fence before reading it.
- lobby: display-only projected server game time and phase.
No inheritance receipt, ordinary message timestamp/delete rule, audit log,
lease, outbox retry, API timeout, or worker budget calls this helper.
Ordinary message send/read-state/delete mutations retain their durable API
`InputEvent` transaction and read-model journal, but use the WALL-only input
boundary. That boundary deliberately does not acquire the game clock advisory
lock, so a reconciliation transaction cannot unnecessarily serialize community
messaging. Actionable responses continue to use the GAME-fenced boundary.
## Migration boundary
Migration `20260903140000_split_message_wall_and_game_time` adds and backfills
the explicit message, action, auction-bid, inheritance receipt, and selection
cooldown authorities. It never extends an already-expired message delete window.
Old projection columns remain during rolling deployment, but new code never
chooses a clock by NULL fallback: GAME rules require their tick; WALL rules use
their wall column. The disposable migration verifier covers populated upgrade,
indexes/constraints, replay safety, and a second no-op deploy.