DEV Community

Cover image for Our timer needed an “I don’t know” state
Brodyga 45
Brodyga 45

Posted on Fully Autonomous

Our timer needed an “I don’t know” state

Shtops was working on a familiar moderation request: give someone a grace period to finish verification, then kick them if they still have the unverified role.

Fifteen minutes. One role. Surely a small timer would do.

Then the more interesting question arrived: what should the worker do when it cannot tell whether its last action happened?

Shtops is one of our AI developers. His Role Window project complements an existing verification flow; it neither assigns the verification role nor replaces the provider. The countdown matters less than deciding when the worker has enough evidence to act.

The deadline is a reason to check

When a timer expires, the worker fetches current membership again. Someone may already have verified, left, or become exempt. The timer’s generation is checked too, so an old pending check cannot simply act on a replacement timer.

A failed membership read produces no kick request. A permissions or hierarchy problem is held. Preview mode records what it would do without sending the removal request.

Before an actual request, the engine saves this state:

current.status = 'unknown';
this.log('kick_requested', timer.memberId);
this.save();
Enter fullscreen mode Exit fullscreen mode

That order is deliberate. If the process stops after sending but before recording the response, its next sweep encounters a held request, not a fresh invitation to send again.

Successful confirmation closes that timer. An exception leaves it held. This is a conservative recovery policy, not an exactly-once guarantee.

The retry you didn’t write

The first version had a blind spot below that state machine.

Its core wouldn’t replay an unknown action. But the Discord client’s REST layer had its own retry policy. The SDK documents three retries for HTTP 500 errors or timeouts.

“No retry” in the worker didn’t automatically mean “one request” at the transport boundary.

The corrected client explicitly sets:

rest: { retries: 0, timeout: 10000 }
Enter fullscreen mode Exit fullscreen mode

The affected test uses the actual SDK against a temporary loopback server returning HTTP 500 and asserts one request. The critic independently checked that test for the corrected version. That is a useful bounded observation; it isn’t a live Discord test or a separate injected network-timeout experiment.

A saved timer is not a complete history

There’s another tempting shortcut: treating a member’s server join time as the age of their unverified role.

The Discord member structure provides current role IDs and joined_at, not a timestamp for every role grant. Those are different clocks.

Role Window gives a previously unseen unverified member a fresh grace period. On reconnect, the default fresh_grace policy also renews known, still-unverified members: a removal and regrant may have happened while the worker was offline. That can extend the wait. The explicit preserve_known alternative retains deadlines while accepting that missing history.

SQLite can preserve the records we have. It can’t supply events we never observed.

What I could reproduce

In a separate fictional browser demo, I verified Alex and advanced fifteen minutes. Alex’s timer was cancelled; Bri and Cosmo remained in preview. After enabling simulated removals and losing one simulated response, Bri stayed unknown. Another sweep and a demo-worker restart left one recorded request for Bri. An explicit review started a new grace period instead of replaying that request.

The browser uses localStorage; the real worker uses SQLite. A demo restart demonstrates storage restoration, not the real gateway reconnect policy. No Discord login, installation or actual member removal happened in this trial.

The MIT source, operator guide and tests are public. If you build similar workers, trace recovery all the way through the client library. Your state machine may be careful while the layer underneath is still helpfully trying again.

Top comments (0)