| D-M8-05-1 |
2026-10-24 |
Settle(now, newMsgs, lookback, starter, SettleConfig) in cmd/bothy/importtool/settle.go returns the copy, the number held and a HoldReason (busy, unsettled, lookback_busy). Only messages of a taken type (22 6.1) can stop the copy; other types pass through and the walker counts them as skipped. A lookback message stops the thread when it is busy, whatever its type. A timestamp that does not parse falls back to the time of the message ID. The round reads the starter message of each type 21 message once per round (GET around its ID); a starter that answers 403 or 404 counts as not busy. Settle is applied page by page; the first page that holds a message ends the reading of the thread, which equals the rule over the whole list because the copy is a prefix. |
Brief design note 1, 22 12.3. |
| D-M8-05-2 |
2026-10-24 |
Every round reads the source with getImport and takes the leads from its stored mapping; a dry mirror without a source takes the leads of the file. Only channels with lead discord are read; a channel with lead bothy whose main thread or an active thread shows a message after the cursor gets the note "written in Discord after the switch-over" and no call beyond the lists. |
Brief design note 2 b, 22 12.1, 12.2 item 4. |
| D-M8-05-3 |
2026-10-24 |
The wait before a round is min(import.follow.max_backoff, import.follow.poll_interval * 2^failures), failures counting the failed rounds in a row and reset by a success. The waits are slept through Env.FollowSleep in pieces of at most import.follow.poll_interval with a lease renewal after each piece (the ticker of M8-04 renews during a round); a renewal that fails counts as a failed round, a lease another run took over ends the mirror with exit 3. Thread-local errors are a 403 or 404 of Discord and a 4xx of Bothy except 401, 429 and 409 conflict with reason import_not_running; they become a note and the round goes on. Every other error fails the round; --once then exits 1. |
Brief design note 3, 22 7.4, 12.2 item 5. Renewing inside the wait keeps the guarantee deterministic on a fake clock. |
| D-M8-05-4 |
2026-10-24 |
A source that is paused or draft makes the round a no-op with a note; a mirror may also start on such a source (a scheduled --once after the owner paused it exits 0 with the note) and takes the lease only in its first round on a running source; completed ends the mirror with exit 0, failed with exit 1. SIGINT and SIGTERM cancel the context of Main: the round runs on a context without that cancel, the walker finishes the current batch and stops, and the lease is cleared; a source that is not running keeps its lease until it is stale, because setImportCursor refuses it. |
Brief design note 3. |
| D-M8-05-5 |
2026-10-24 |
A dry mirror (--follow or --once without --apply) holds no lease and writes nothing; it reads the stored cursor and leads with getImport and prints the report of every round with "would copy". --once without --apply is one such round. --follow with --apply prints a round only when it copied, held or noted something; --once prints its round. --only-thread is refused only with --follow (22 10.2 rule 3). |
Brief design note 4, 22 10.1, 10.2. |
| D-M8-05-6 |
2026-10-24 |
The cursor writes of the mirror carry no counters, so stats.tool keeps the totals of the last full run (the server merges the members it receives); the round's counts are in its report, with the new member messages_held (0 in a full run, 22 11). |
A mirror running for days would otherwise replace the import totals with the counts of one round. |
| D-M8-05-7 |
2026-10-24 |
The cursor object keeps, besides the version-1 members of D-M8-04-8, an optional member mirrored: per thread a fingerprint of the last posted thread item (o: or c: and the first 8 bytes of the SHA-256 of the name in hex, never the name). Full runs and the mirror write it after each posted thread item; a round posts a thread item only for a new thread, a fingerprint that differs from Discord's view, or new messages. A thread the fingerprint knows as open that is missing from the active list is confirmed with GET /channels/{id}; a thread known as done and not active is not read again. After an import by an earlier version every thread item is posted once. |
22 12.2 item 3 without reading every thread every round; --once runs in a fresh process, so the state must live in the cursor. |
| D-M8-05-8 |
2026-10-24 |
The mirror starts like a full run: preflight, plan with the archived thread lists, and with --apply the structure batches and the posted files once before the first round (so a mirror may start before or after the full import). New threads come from the active list; a thread created and archived between two rounds is taken over by the next full run. Env.FollowSleep is a new member of Env (nil means the rate-limit sleeper). |
D-W-100; the brief names no startup. |
| D-M8-05-9 |
2026-10-24 |
A new thread of a text channel (no fingerprint yet) waits while the main thread still has messages from the thread's ID on that this round did not copy (held by the settle rule, or a failed read): the server links a thread to its starter message only when the starter is posted first, and a re-posted thread item never adds the link. The thread is posted in the round that copies its starter. A forum (no main thread) gets the lead=bothy warning only from its threads. |
Review finding of M8-05 (a thread created on a held message lost its parent for good); 22 5.1.1. |