AWOL System¶
What it does¶
Identifies members whose recent activity has fallen below the configured thresholds, assigns the AWOL role, posts a notification to the AWOL channel after a grace period, and provides a senior-officer command to kick those members.
Reserve role members are fully exempt from every step.
Activity thresholds¶
A user is flagged AWOL when they have both fewer than MinMessages (default 5) and less than MinVoiceHours (default 1.0) within their activity window. Meeting either threshold keeps them safe.
The window is WindowDays (default 28) for everyone, except members holding any role in ShortWindowRoles (default Guest,RCT), who use ShortWindowDays (default 14). The shorter window means new members and guests are evaluated on a faster cycle.
flowchart LR
Start[Member checked] --> Exempt{Has exempt role?}
Exempt -->|Yes| End[Skip]
Exempt -->|No| Reserve{Has Reserve role?}
Reserve -->|Yes| End
Reserve -->|No| Window{Get window<br/>days}
Window --> Count[Count messages<br/>and voice hours<br/>in window]
Count --> Both{Both below<br/>threshold?}
Both -->|No| End
Both -->|Yes| Already{Already AWOL?}
Already -->|Yes| Skip[Update last seen]
Already -->|No| Assign[Assign AWOL role,<br/>create AwolRecord]
Lifecycle¶
stateDiagram-v2
[*] --> Active
Active --> Flagged : activity below thresholds
Flagged --> Notified : grace period elapsed,<br/>HQ post succeeds
Flagged --> GivenUp : 7 days of failed<br/>notification attempts
Notified --> Kicked : /kick-awols by officer
Notified --> Cleared : /clear-awol or<br/>activity recovered
Flagged --> Cleared : same
Cleared --> Active
GivenUp --> [*]
Kicked --> [*]
State transitions¶
| From | To | Trigger |
|---|---|---|
| Active → Flagged | AwolCheckService cycle finds activity below thresholds. AWOL role assigned, AwolRecord row created. |
|
| Flagged → Notified | AwolGraceDays (default 2) have elapsed since AssignedAt. Notification posted to HqChannelName. NotificationSent=true. |
|
| Flagged → GivenUp | 7 days have elapsed since AssignedAt and notification has failed every time. Record auto-resolved. |
|
| Notified → Kicked | Senior officer runs /kick-awols. Audit row written to AwolKickAuditRecord. |
|
| Notified/Flagged → Cleared | Officer runs /clear-awol, or activity recovers above thresholds. AWOL role removed, record marked resolved. |
Components¶
| File | Responsibility |
|---|---|
AwolCheckService.cs |
Background loop. Runs every CheckIntervalMinutes. Identifies AWOL candidates, assigns/clears the role, posts notifications. |
AwolRecord (entity) |
One row per current AWOL state per user. |
AwolKickAuditRecord (entity) |
Audit row per kick. |
KickAwolsCommandHandler.cs |
/kick-awols slash command (MAJ+). |
ClearAwolListCommandHandler.cs |
/clear-awol-list slash command (MAJ+). |
SlashCommandHandler.cs |
/awol-status, /awol-check, /clear-awol |
Notification retry & give-up policy¶
AwolRecord.LastNotificationAttemptUtc is stamped on every attempt to post the notification — successful or not. This lets the service distinguish three states:
NotificationSent |
LastNotificationAttemptUtc |
Meaning |
|---|---|---|
false |
null |
Fresh record, grace period not yet elapsed. |
false |
DateTime |
Has tried at least once, all attempts failed. |
true |
DateTime |
Successfully notified. |
Records in row 2 that were assigned 7+ days ago are auto-resolved as "given up" — they stop occupying the pending queue. If the underlying problem (deleted channel, revoked permissions) is later fixed and the user is still inactive, a fresh AwolRecord will be created on the next cycle.
Reserve exemption¶
Reserve members have an explicit doctrinal meaning ("active but paused") and are tracked separately from ExemptRoles for that reason. The check is in AwolCheckService and looks for the role named in BotConfig.ReserveRoleName (default Reserve). Reserve members:
- Are never assigned the AWOL role.
- Are never kicked by
/kick-awols. - Can still use
/awol-statusand see their stats.
If you wanted to convert someone to a long-term exemption, give them a role from ExemptRoles instead. Reserve is for temporary pauses.
Kicking¶
/kick-awols (MAJ+) iterates the current AWOL set, skips Reserve, kicks each member from the guild, and writes an audit row per kick to AwolKickAuditRecord.
flowchart TB
Cmd[/kick-awols] --> Auth{Invoker MAJ+?}
Auth -->|No| Deny[Reject ephemeral]
Auth -->|Yes| Iter[For each AwolRecord<br/>where notified]
Iter --> Reserve{Has Reserve role?}
Reserve -->|Yes| Skip[Skip, log reason]
Reserve -->|No| Notified{Notification sent?}
Notified -->|No| Skip
Notified -->|Yes| Kick[Kick member]
Kick --> Audit[Write AwolKickAuditRecord]
Audit --> Iter
Self-healing¶
AwolCheckService reconciles state every cycle. If a member has the AWOL role but no AwolRecord (e.g. role was assigned manually, or DB was restored from backup), one is created. If a member has an AwolRecord but no AWOL role, the record is closed out as cleared.
Common operational questions¶
Why didn't user X get flagged when they're clearly inactive?
Most common causes:
- They have an
ExemptRolesrole (Admin, Moderator, Retired, Bot, Bot Whisperer). - They have the Reserve role.
- Their window hasn't fully elapsed yet — new members get a grace period as their window opens.
- They have either ≥
MinMessagesmessages or ≥MinVoiceHourshours. Meeting either keeps them safe.
Why did the same notification post twice?
A bot restart between "post" and "save NotificationSent=true". The notification posted, but the DB write didn't commit. On the next cycle the record looks unsent, so it posts again. Mitigation: the post-then-save sequence is order-of-operations critical and should be reviewed if this happens repeatedly.
Can I bulk-clear the AWOL list?
/clear-awol-list (MAJ+) clears the entire pending queue. Use with care — it removes the AWOL role from everyone and deletes all AwolRecord rows.
Where do I see the kick history?
AwolKickAuditRecord table. There's no slash command surface for it yet, but you can query directly:
sql
SELECT * FROM AwolKickAuditRecord ORDER BY KickedAt DESC LIMIT 50;