Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/plans/2026-07-23-discord-7d2d-blood-state-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Discord 7D2D Blood State Design

## Goal

Keep the private first-player Blood Moon notice current even when Discord delivery fails, while retrying failed Discord announcements without duplicating successful ones.

## State model

- `discord7d2d:bloodState` stores only the currently observed phase used by the join hook.
- `discord7d2d:bloodDelivered` stores up to 32 unique announcement keys that Discord has actually accepted.
- `discord7d2d:bloodPending` stores up to 32 unique, chronologically ordered announcements that still need delivery.
- `discord7d2d:bloodMonitorLock` serializes the complete observation and delivery state machine per server and module.
- Announcement keys remain `today:<day>`, `start:<day>`, and `end:<day>` so legacy `bloodState` values can migrate without translation.

Each cron execution first derives and persists delivered history, including migration of any announcement-shaped legacy `bloodState`. Only after that migration is durable does it overwrite the observed phase used by the join hook. It then merges newly due announcements into pending state, persists the merged pending list, and processes it in order. This order prevents a crash after the observed-state write from making an unsent current announcement look like a delivered legacy announcement on the next run. Each successful announcement is recorded in delivered history before it is removed from pending state. A failed send or an unconfigured-channel skip remains pending across later phase and day transitions without repeating earlier successes.

Delivery is intentionally at least once across the external side-effect boundary: if Discord accepts a message and the function crashes before the delivered-history write completes, a later retry can duplicate that message. Discord does not provide an idempotency key for this send operation, so the module prefers a visible duplicate over permanently losing an announcement.

Before reading game time or any state, each cron atomically creates the scoped lock with a unique owner token and expiry. A competing execution uses bounded 50-250ms backoff and never enters the state machine until it owns the lock. Because Takaro's function VM does not expose timers, the backoff uses standard `Atomics.wait` rather than issuing artificial API traffic. The owner re-checks and renews its lease after status retrieval, after state reads, before each state mutation, and around every pending send. The 120-second lease is comfortably above Takaro's 30-second production function limit, while renewal also protects longer local executions. Expired or malformed locks are reclaimed by record ID, and `finally` releases only a lock whose current owner token still matches. Acquisition timeout and non-404 release failures remain visible execution failures. Ordinary variable creation also recovers from a create conflict by re-reading the same scoped key and updating the winning record after a small bounded backoff.

Takaro queue jobs contain an installation snapshot captured when they are enqueued. Reinstalling while an older job is queued cannot cancel that job, so after acquiring the lock the monitor fetches the current module installation and uses its current user and system config for time parsing, phase decisions, and channel resolution. A queued execution that outlives an uninstall receives a 404, exits without mutating state, and still releases its lock.

## Phase and announcement derivation

Observed state and announcements are separate outputs. For ordinary intervals, behavior remains today, start, and end at the existing boundaries. When consecutive days are Blood Moon days, the previous Blood Moon remains active until the configured end hour. At or after that boundary, the current observed state becomes `today:<current day>` and the cron can expose both `end:<previous day>` and `today:<current day>` as ordered announcements.

For interval `1`, day 8 at 05:00 therefore produces:

- observed state: `today:8`;
- announcements: `end:7`, then `today:8`.

## Migration

When the delivered-history variable does not exist, any legacy announcement-shaped `bloodState` value is recorded as delivered before current pending work is compared. The old implementation wrote that state only after successful or skipped delivery, so this prevents a current announcement from repeating immediately after upgrade. Normal and unknown legacy states do not imply a delivered announcement.

## Integration proof

The active WebSocket mock server has no configurable `gettime` response. The disposable test import will extend only its config schema to permit deterministic `say Day <n> <time>` commands. The production code will still execute through Takaro's real command API, and the real mock server returns a parseable `Sent message: Day ...` response. No helper methods, Takaro clients, or module source will be mocked.

Tests will prove migration-before-observation crash ordering, observed-state-before-pending/delivery behavior, retry bookkeeping, interval-one overlap, stale-lock recovery, bounded acquisition timeout that preserves another owner, current-config resolution for an older queued job, uninstall-gap handling, and concurrent serialization without lost pending keys through real Takaro variables and cron executions. The disposable import moves automatic Blood Moon scheduling to a valid dormant expression based on the prior UTC day, whose next occurrence is outside the test run, so every execution under test is explicitly triggered; the production manifest schedule is asserted separately. A configured forbidden Discord channel remains environment-gated.
78 changes: 78 additions & 0 deletions docs/plans/2026-07-23-discord-7d2d-blood-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Discord 7D2D Blood State Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Separate observed Blood Moon phase from Discord delivery history so private notices stay current and failed announcements retry without duplicates.

**Architecture:** Continue storing the observed phase in `discord7d2d:bloodState` for join-hook compatibility. Store up to 32 delivered keys in `discord7d2d:bloodDelivered` and 32 chronologically ordered retry keys in `discord7d2d:bloodPending`; persist observation, migration, and merged pending state before sending, then record each successful announcement before removing it from pending.

**Tech Stack:** JavaScript Takaro functions, TypeScript Node test runner, real Takaro API, Takaro WebSocket mock game server.

---

### Task 1: Prove state, retry, migration, and consecutive-day behavior

**Files:**
- Modify: `modules/discord-7d2d-status-bridge/test/bridge.test.ts`

**Step 1: Extend only the disposable imported schema**

Allow deterministic test commands such as `say Day 7 12:00` and `say Day 8 05:00` in the disposable module's `timeConsoleCommand` enum. Keep the production schema unchanged.

**Step 2: Write failing real-Takaro tests**

- Run a cron with a parseable day/time response and assert observed state plus delivered-history values.
- Seed a legacy announcement-shaped state with no delivered history and assert no duplicate current announcement.
- Assert real variable-operation logs persist migrated delivery history before overwriting observed state, then persist pending work before Discord.
- For interval `1` at day 8 05:00, assert observed `today:8` and ordered completion of `end:7`, then `today:8`, once each.
- Advance a failed `today:7` execution to `start:7` and assert both remain pending in chronological order.
- Trigger concurrent cron executions against absent variables and assert create conflicts recover without duplicate keys.
- Seed an expired monitor lock and assert it is reclaimed and released through the real variable API.
- Trigger concurrent cron executions with existing pending work and assert one waits for the scoped lock without losing any keys.
- Seed another active owner and assert acquisition backs off for the bounded interval, fails clearly, and preserves that owner's record.
- Hold an older queued execution across reinstall and assert it resolves the current installation config instead of writing stale state.
- Uninstall while a queued execution is held and assert the eventual 404 exits without state mutation while releasing the lock.
- With the environment-provided forbidden Discord channel, assert the cron fails after persisting observed state, leaves the failed announcement pending, retries it, and the join hook can still PM from the current observed state.

**Step 3: Run RED**

Run the focused integration tests through the real Takaro API with Bearer/Authorization redaction. Expect failures because delivery history does not exist, observed state is currently written after Discord, and interval-one phase derivation returns `end:7` all day.

### Task 2: Implement separated state and delivery tracking

**Files:**
- Modify: `modules/discord-7d2d-status-bridge/src/cronjobs/blood-moon-monitor/index.js`
- Modify: `modules/discord-7d2d-status-bridge/src/functions/discord-7d2d-status-helpers.js`

**Step 1: Add delivered-history state**

Export `BLOOD_DELIVERED_KEY` and `BLOOD_PENDING_KEY`. Normalize both stored lists to unique announcement keys capped at 32 entries.

**Step 2: Derive observation and announcements separately**

Return one observed key and an ordered list of announcement descriptors. Consecutive-day end overlap must yield `today:8` plus `end:7`, `today:8` at day 8 05:00.

**Step 3: Order persistence and delivery**

Acquire a scoped unique variable lock before reading game time or state. Use an owner token plus expiry, bounded polling with 50-250ms backoff, stale deletion by record ID, owner-checked renewal around external work and mutations, and owner-checked `finally` release. Keep the lease above Takaro's production function runtime bound. Fetch the current installation after lock acquisition so an older queued snapshot cannot apply stale user or system config; treat a 404 during an uninstall gap as a safe no-op. Read legacy state/history, derive and persist migrated delivery history before overwriting observed state, then merge and persist pending work before sending announcements in order. Persist each successful key before removing it from pending; keep failures and skips pending. Recover scoped variable create conflicts with a bounded re-search and backoff.

**Step 4: Run GREEN and refactor**

Run the focused state tests, then the complete module suite. Keep current cron timing and Discord diagnostics unchanged.

### Task 3: Verify and commit

**Files:**
- Verify: `modules/discord-7d2d-status-bridge/**`

**Step 1: Static verification**

Run `npm run build`, `npm run typecheck`, both changed JavaScript files through `node --check`, and `git diff --check`.

**Step 2: Focused integration verification**

Run the complete bridge integration suite with output redaction. Report any environment-gated Discord cases separately.

**Step 3: Commit**

Commit source and tests with a message describing reliable Blood Moon state tracking.
47 changes: 47 additions & 0 deletions docs/plans/2026-07-23-discord-7d2d-monitoring-feedback-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Discord 7D2D Monitoring Feedback Design

## Goal

Make `discord-7d2d-status-bridge` behave honestly and usefully in the user's real setup: monitoring delivery must either reach the configured Discord text channel or explain exactly why Discord rejected it, Blood Moon phase notices should be detected promptly, and an optional private Blood Moon notice should be sent when the first player joins.

## Confirmed failure

The user's `updateStatus` execution successfully reads players, server details, and `gettime`, then receives HTTP 403 from `POST /discord/channels/:id/message`. `bloodMoonMonitor` reports success during ordinary phases because it updates state without sending a Discord message. Existing integration tests use an empty monitoring channel and therefore exercise the skip path rather than outbound Discord delivery.

The module cannot grant Discord permissions or make Takaro's bot join an inaccessible private or archived thread. It can preserve the channel-ID/Takaro-bot flow, expose a direct delivery check, and turn generic 403 failures into actionable diagnostics. Real acceptance still requires a regular Discord text channel that the Takaro bot can view and send to.

## Design

### Discord monitoring delivery

- Keep `monitoringChannelId`; do not introduce webhooks.
- Centralize Discord API error classification in the shared helper.
- For HTTP 403, log and throw an actionable error that names the channel and asks the operator to use a normal text channel and grant the Takaro bot `View Channel`, `Send Messages`, and `Read Message History`. Explicitly call out private or archived threads.
- Preserve other API errors and include their status/code without leaking credentials.
- Do not silently fall back to the chat channel, because that would break the requested channel separation.

### Blood Moon timing

- Change `bloodMoonMonitor` from every five minutes to every minute. The cron remains transition-based, so it sends at most once per phase and does not spam.
- Keep status-message refresh at five minutes to avoid unnecessary player/time/API traffic.
- Document that interval `1` means every in-game day; detection happens on the first one-minute cron tick after the configured in-game boundary.

### Private first-player notice

- Add `privateBloodMoonNoticeOnFirstJoin`, defaulting to `false` so existing installations do not gain unsolicited private messages.
- Add an optional `privateBloodMoonTodayMessage` override with English and Polish presets.
- The one-minute Blood Moon monitor remains the source of truth by persisting its current phase. On `player-connected`, read that phase and query only the online-player list. If the option is enabled, the phase is `today` or `start`, and the joining player is the only online player, send the resolved message with `player.pm`.
- Discord join monitoring and the private in-game notice remain independent: a Discord delivery failure must still fail visibly, while the private notice uses the game messaging path.

## Testing

- Extend the real Takaro integration suite first and observe failures before implementation.
- Replace empty-channel false positives for monitoring behavior with assertions that distinguish `skipped` from attempted delivery.
- Use a configured QA Discord channel for a real send when `TAKARO_DISCORD_TEST_CHANNEL_ID` is present; never fake the Takaro helper or replace source strings.
- Cover the actionable 403 diagnostic through a real execution log where the environment provides an inaccessible channel.
- Cover first-player private notice, non-Blood-Moon suppression, and second-player suppression through real Takaro hooks and event/game logs.
- Run TypeScript build, focused integration tests, export conversion, and mandatory Paper/bot verification. A live 7D2D run is required for precise `gettime`/Blood Moon claims; Paper proves only the game-agnostic join/private-message and Discord delivery paths.

## Delivery boundary

The module fix is complete only when source and real-Takaro tests pass and a real Discord POST succeeds in the QA channel. The affected user's channel can still require a one-time Discord configuration correction if Takaro receives 403 for that specific channel; the new command and error text will identify that directly.
Loading
Loading