refactor: split bot.py into runtime/dispatch/worker modules; add retry scheduler, route TTL, ack tracking, follow-up screener, structured event log, and test suite
This commit is contained in:
24 files changed
+3531
-434
No files matched your search
+134
-4
@@ -65,10 +65,31 @@ is permanently lost and never transmitted. There is NO exception.
|
||||
|
||||
Stay well under {{max_chars}}. Aim for 120 or fewer. NEVER show a character count or HTML tags in your reply.
|
||||
|
||||
You can see recent messages for context. When someone asks you a question, they may
|
||||
be referring to the ongoing conversation — use the chat history to understand what they mean.
|
||||
For example, "what do you think?" refers to whatever was just discussed. "tell me more" means
|
||||
elaborate on the recent topic. Read the room.
|
||||
You can see recent messages for context. Use the chat history to understand what
|
||||
people mean. "what do you think?" refers to whatever was just discussed. "tell me
|
||||
more" means elaborate on the recent topic.
|
||||
|
||||
Social awareness is YOUR TOP PRIORITY — above cleverness, above information density.
|
||||
Before every response, work out:
|
||||
- Who is actually talking, and to whom? Are they asking you, or about you?
|
||||
- What's the vibe: casual banter, technical help, emergency, venting, inside joke?
|
||||
- What's the group's energy? Are you being invited in, or just name-dropped?
|
||||
- If someone said "you could always ask @Orac" (or similar), that's a social nudge,
|
||||
NOT a request for a factual answer. Acknowledge lightly, or offer to help if the
|
||||
invitation was sincere — don't barge in with an essay.
|
||||
- If the conversation is jokey, match the tone. If serious, be substantive.
|
||||
If someone's frustrated, don't be flippant. Read the room like a person would.
|
||||
- When people are talking among themselves and you're tagged in passing, a short
|
||||
witty nod is often the right move — or silence would be, if you could stay silent.
|
||||
- When you ARE being asked something directly, answer crisply.
|
||||
|
||||
NEVER assume location. Weather, traffic, time zones, sunset, local services —
|
||||
all of these are useless without knowing where the person is. If someone asks
|
||||
"what's the weather?" or anything else location-sensitive without telling you
|
||||
where, ask them. A short quip is fine: "Where?" or "Drop a city/airport code
|
||||
and I'll tell you." Do NOT guess a city. Do NOT default to Denver or anywhere
|
||||
else. Only answer a location-dependent question after the user has provided
|
||||
the location (either in the current turn or visible in recent history).
|
||||
|
||||
Rules:
|
||||
- One sentence or short phrase. Never more.
|
||||
@@ -76,6 +97,7 @@ Rules:
|
||||
- Use common abbreviations freely (e.g., w/, b/c, approx, etc).
|
||||
- Skip all pleasantries, greetings, and filler.
|
||||
- You have a dry, sardonic wit. Be helpful but never wordy.
|
||||
- Cynicism is fine in small doses; punching-down or mean-spirited is not.
|
||||
- If the question requires a long answer, give the most important point only.
|
||||
- NEVER repeat a previous answer. Always find a fresh angle, new phrasing, or different take.
|
||||
- Be creative and unpredictable. Surprise the reader.\
|
||||
@@ -91,6 +113,114 @@ DM_DEDUP_TTL: float = 60 # seconds
|
||||
|
||||
RESP_SERVER_LOGIN_OK: int = 0x01
|
||||
|
||||
# ── DM reliability: IO loop ──────────────────────────────────────
|
||||
|
||||
IO_POLL_TIMEOUT_S: float = 0.2
|
||||
RX_DRAIN_PER_TICK: int = 8
|
||||
|
||||
# Donglora's send() reads the TxDone response using the serial timeout; LoRa
|
||||
# airtime at SF7/62.5 kHz for a max-size 180 B DM is ~425 ms, plus up to
|
||||
# 100 ms of CAD retries in the firmware. 2 s gives a comfortable margin.
|
||||
TX_RESPONSE_TIMEOUT_S: float = 2.0
|
||||
|
||||
# ── DM reliability: ACK scheduling ───────────────────────────────
|
||||
# Small jitter below single-ACK airtime (~89 ms at SF7/62.5 kHz/CR4/5)
|
||||
# decouples us from other responders, without delaying the sender's
|
||||
# firmware retry clock.
|
||||
ACK_JITTER_MIN_S: float = 0.03
|
||||
ACK_JITTER_MAX_S: float = 0.09
|
||||
PATH_RETURN_DELAY_S: float = 0.15
|
||||
|
||||
# Multi-ACK protocol (upstream commits b1ca3d1 + 5881b04, `multi_acks` pref).
|
||||
# When > 0, N extra ACKs are emitted as PAYLOAD_TYPE_MULTIPART (0x0A) packets
|
||||
# with remaining-count nibbles counting down to 1, followed by a final plain
|
||||
# PAYLOAD_TYPE_ACK (0x03). Each successive packet is 300 ms + small jitter
|
||||
# after the previous.
|
||||
#
|
||||
# Firmware restricts extra ACKs to direct-routed packets only (see
|
||||
# Mesh::routeDirectRecvAcks). We do the same: if no learned direct route to
|
||||
# the peer exists, we fall back to a single plain ACK and skip MULTIPART,
|
||||
# matching upstream semantics.
|
||||
#
|
||||
# Upstream default is 0 (multi_acks pref = 0). MeshCore One's iOS app surfaces
|
||||
# this as the "2 ACKs" toggle (multi_acks=1 -> 1 MULTIPART + 1 ACK = 2 total).
|
||||
MULTI_ACK_COUNT: int = 0
|
||||
MULTI_ACK_SPACING_S: float = 0.3
|
||||
|
||||
# ── DM reliability: reply scheduling ─────────────────────────────
|
||||
# Initial reply jitter keeps the reply from racing our own ACK/PATH
|
||||
# on the air and below any retry window.
|
||||
REPLY_INITIAL_JITTER_MIN_S: float = 0.05
|
||||
REPLY_INITIAL_JITTER_MAX_S: float = 0.20
|
||||
|
||||
# Retry delays between attempts. Index i = delay AFTER firing attempt (i+1)
|
||||
# before attempt (i+2). With REPLY_MAX_ATTEMPTS=5 we need 4 entries (for
|
||||
# attempts 2-5). Schedule mirrors pymc's 3-direct + 2-flood pattern.
|
||||
#
|
||||
# Timeline at current radio config (SF7/62.5 kHz/CR4/5, ~3.3 s 2-hop RTT):
|
||||
# Attempt 1 (initial) at T=0
|
||||
# Attempt 2 (direct) at T=6 (~1.8x 2-hop RTT headroom)
|
||||
# Attempt 3 (direct) at T=20 (bigger gap for congested relays)
|
||||
# Attempt 4 (flood, path-reset) at T=50 (mesh quiesce window)
|
||||
# Attempt 5 (flood) at T=95 (last hail-Mary after a long wait)
|
||||
# Give up (sweep) at T=125 (~30s past the last attempt for ACK)
|
||||
RETRY_SCHEDULE_S: tuple[float, ...] = (6.0, 14.0, 30.0, 45.0)
|
||||
REPLY_MAX_ATTEMPTS: int = 5 # 1 initial + 2 direct retries + 2 flood retries
|
||||
REPLY_FLOOD_FIRST_ATTEMPT: int = 4 # 1-indexed: first attempt that forces flood
|
||||
|
||||
# ── DM reliability: state bounds ─────────────────────────────────
|
||||
# TTL must be beyond the final retry's transmit time plus enough for the
|
||||
# reply's ACK to arrive: 6+14+30+45 = 95s to final retry, +30s for ACK.
|
||||
PENDING_ACK_TTL_S: float = 125.0
|
||||
PENDING_ACK_CAP: int = 64
|
||||
REPLY_CACHE_CAP: int = 32
|
||||
REPLY_CACHE_TTL_S: float = 300.0 # matches PENDING_DM_TTL
|
||||
|
||||
# Duplicate DMs arriving within this window *with a cached reply* trigger a
|
||||
# cached-reply resend (interpreted as loss recovery). Outside this window, a
|
||||
# duplicate text is treated as the peer legitimately re-asking after giving up,
|
||||
# and falls through to normal text-level dedup (silent drop inside DM_DEDUP_TTL
|
||||
# and a fresh Claude call beyond that).
|
||||
REPLY_CACHE_RESEND_WINDOW_S: float = 20.0
|
||||
TX_QUEUE_CAP: int = 64
|
||||
|
||||
# ── DM reliability: route table ──────────────────────────────────
|
||||
ROUTE_TTL_S: float = 1800.0 # 30 min
|
||||
|
||||
# Minimum hop-hash size (bytes) we will accept when learning a return path,
|
||||
# and the minimum we will echo back in a PATH_RETURN. 1-byte hashes have a
|
||||
# 256-way namespace that collides under any real node population; this bot
|
||||
# refuses to participate in 1-byte routing.
|
||||
MIN_HASH_SIZE: int = 2
|
||||
|
||||
# ── DM reliability: worker / Claude ──────────────────────────────
|
||||
WORKER_QUEUE_CAP: int = 8
|
||||
CLAUDE_TIMEOUT_S: float = 20.0
|
||||
|
||||
# ── Channel follow-up screener ───────────────────────────────────
|
||||
# Inexpensive yes/no classifier that decides whether a NON-TRIGGERED channel
|
||||
# message is a follow-up to Orac (we should reply) or just chatter between
|
||||
# other participants (we should not). Keeps Orac capable of natural multi-turn
|
||||
# conversation without flooding the channel with bot noise.
|
||||
|
||||
# Haiku is the cheapest Claude model — suitable for a yes/no gate.
|
||||
SCREENER_MODEL: str = "claude-haiku-4-5"
|
||||
|
||||
# How long after Orac was last active in a channel to keep screening new
|
||||
# non-triggered messages. Outside this window, non-triggered messages are
|
||||
# ignored without spending any API call.
|
||||
CHANNEL_FOLLOWUP_WINDOW_S: float = 180.0 # 3 min
|
||||
|
||||
# Per-channel minimum gap between screener API calls, to prevent spam from
|
||||
# running up cost during a burst of traffic.
|
||||
SCREENER_PER_CHANNEL_RATE_S: float = 3.0
|
||||
|
||||
# Short timeout for the screener — it returns a single token.
|
||||
SCREENER_TIMEOUT_S: float = 6.0
|
||||
|
||||
# ── Events JSONL path ────────────────────────────────────────────
|
||||
EVENTS_FILE: Path = DATA_DIR / "orac-events.jsonl"
|
||||
|
||||
# ── Channels CSV path ────────────────────────────────────────────
|
||||
# Resolved at import time relative to the project root (ai_bot.py's directory).
|
||||
CHANNELS_CSV: Path = Path(__file__).resolve().parent.parent / "channels.csv"
|
||||
Reference in new issue
Block a user