# Vol V Internals — INBOX ZERO *The engineering manual for `inbox.html`: how plausible mail is minted, how the flood ramps, the overwhelm penalty that nearly broke the economy, the rules engine that automates your judgment, and the Inbox-Zero prestige loop.* INBOX ZERO is Volume V of *Boring Software* — an Outlook 97 three-pane mail client that is secretly a survival game against exponential noise. Where **Vol IV — UPLINK** asks you to balance load against the OOM killer, this volume asks a simpler, crueler question: can you process mail faster than it arrives? The honest answer is *no* — not by hand — and the whole arc is the player discovering that, then encoding their judgment into rules until the inbox empties itself. The save key is `boringsoft_inbox_v1`. The shared scaffolding (`fmt`, the `~100ms` tick, offline catch-up, `sanitizeState`, toasts) is the same contract every volume inherits; this chapter documents only what is specific to the inbox, and assumes you know the DNA from §6 of the style canon. · · · ## The MAIL model A message is a flat object minted by `genMail(forceType)`. The shape it returns: ```js return { id:_mid++, type, from:fromName, addr:fromAddr, subject, value: Math.max(0, baseVal), unread:true, flagged:meta.flag, vip:meta.vip, att: type==='work'&&Math.random()<0.45 || type==='phishing'&&Math.random()<0.6, t: now(), body: pick(BODIES[type]), }; ``` | Field | Type | Meaning | |-------|------|---------| | `id` | int | Monotonic, from the module-level `_mid` counter. Selection (`G.sel`) and DOM rows key off this. | | `type` | string | One of `work`, `newsletter`, `spam`, `phishing`, `vip`. | | `from` / `addr` | string | Display name and `user@domain`, procedurally generated per type. | | `subject` | string | A template from the per-type subject pool, with `{n}` and `{proj}` substituted. | | `value` | number | The *base* Productivity the mail is worth — a per-action multiplier is applied later, never stored here. | | `unread` | bool | Flipped to `false` the moment it's selected (`selectMail`). | | `flagged` / `vip` | bool | `flag`/`vip` from the type's metadata; phishing and VIP both flag. | | `att` | bool | Has an attachment. Decorative, except phishing attachments render as `invoice_overdue.pdf.exe`. | | `t` | epoch ms | Arrival time, drives the `relTime` "3m ago" column. | | `body` | string | A canned paragraph from `BODIES[type]`. | > **DEV NOTE** — `value` is the *base* worth, not the banked Productivity. This is deliberate and load-bearing: a mail minted on the Startup plane and triaged after you've ascended to Government must pay out at *Government* rates. If we'd frozen the multiplier into `value` at spawn time, every mail that survived an ascension would underpay. Multipliers are always applied at the point of triage (`manualMult`, `ruleMult`), never baked in. The reading pane even previews this live: `+fmt(m.value*manualMult()) PP if archived`. ### The procedural generators The minting is unglamorous string assembly, and that is the point — it has to read like real corporate mail, not like a video game. Senders are built from name and domain pools (`FIRST`, `LAST`, `WORK_DOMAINS`, `NEWS_DOMAINS`, `SPAM_DOMAINS`, `PHISH_DOMAINS`, `VIP_DOMAINS`). The flavor is half *The Office* cast (`Schrute`, `Beesly`, `Halpert`) and half spam-folder archaeology (`secure-paypa1.com`, `bank0famerica.net`, `nigerian-prince.org`). Subject lines come from a per-type pool with two placeholders: ```js let subject = pick(subT).replace(/\{n\}/g,n).replace(/\{proj\}/g,proj); ``` `{n}` is a random integer in `[1, 9999)`; `{proj}` is one of the `PROJ` codenames (`Phoenix`, `Atlas`, `Synergy`, …). Address construction is type-specific — work mail derives the address *from* the display name (`fromName.toLowerCase().replace(/[^a-z]/g,'.')`), so "Dwight Schrute" becomes `dwight.schrute@boringsoft.net`, while VIP mail always lands at the current plane's domain (`pick(VIP_DOMAINS)+'@'+plane().domain`). That last detail matters for rules: a `from contains CEO` filter keeps working across planes because the VIP names are stable even as the domain shifts. ### The five types — value and sign `MAIL_TYPES` is the single source of truth for spawn weighting and base worth: | Type | `weight` | `baseVal` | `flag` | `vip` | Triage intent | |------|---------|----------|--------|-------|---------------| | `work` | 34 | 14 | — | — | Archive (the bread and butter) | | `newsletter` | 24 | 5 | — | — | Archive cheaply, or rule away | | `spam` | 24 | 0 | — | — | Delete | | `phishing` | 10 | 0 | ✓ | — | Delete (it's flagged, not VIP) | | `vip` | 8 | 90 | ✓ | ✓ | **Reply** — deleting it costs you | `rollType()` is a standard weighted pick over `TYPE_ORDER` (total weight 100, so the percentages read directly). Base value is then jittered by ±30% so two work mails are never identically priced: ```js const baseVal = meta.baseVal * (0.7+Math.random()*0.6); ``` Note that spam and phishing have `baseVal: 0` — they are worth *nothing* to archive. The "negative value" of spam isn't stored as a negative number; it's expressed structurally, in the asymmetry of `deleteSel`. Deleting a worthless mail pays a tiny "tidied up" reward (`m.value*0.1 + 0.5`); deleting a *VIP* applies a flat penalty of half its banked value and a scolding toast. So the sign of an action is contextual: the same `delete` keystroke is correct on spam and a self-inflicted wound on a VIP. That is the whole risk surface of manual triage in one function. · · · ## The arrival pipeline `spawnMail(n)` is the funnel every new mail passes through, online and offline: ```js function spawnMail(n){ for(let k=0;k lands in inbox G.inbox.push(m); } } if(G.inbox.length && G.sel==null) G.sel = G.inbox[0].id; } ``` The ordering is the design: **rules run before the mail ever touches the inbox.** If a rule matches, `applyRules` banks the Productivity and the mail never enters `G.inbox` (the one exception is `flag`, which re-pushes it — see the rules section). This is why a good rule-set produces a *visibly* self-clearing inbox rather than one that fills and then drains. ### `arrivalRate()` — the flood ```js function arrivalRate(){ const base = 0.8 * plane().rate; const ramp = 1 + Math.min(8, (G.peakThroughput>0? Math.log10(1+G.peakThroughput)*0.18 : 0)); return base * ramp; } ``` Base is `0.8` emails per second, multiplied by the current plane's `rate`. The `ramp` is a slow within-run time-pressure curve keyed off `peakThroughput` — the better you've been doing this run, the faster mail comes, capped at a 9× multiplier (`1 + min(8, …)`). The log-scaling means the ramp is brutal early and forgiving late; it exists to keep a stagnant inbox from being trivially idle-able while you're not investing. `step(dt)` accumulates fractional arrivals so the rate is honored even at sub-1/s on the Startup plane: ```js G.spawnAccum += rate*dt; let toSpawn = Math.floor(G.spawnAccum); if(toSpawn>0){ G.spawnAccum -= toSpawn; if(toSpawn>400) toSpawn=400; spawnMail(toSpawn); } ``` The `toSpawn>400` clamp is a per-tick ceiling so a throttled tab that hands us a 5-second `dt` (the tick's own cap) on The Void can't try to mint thousands of objects in one synchronous loop and jank the frame. Offline catch-up uses the same accumulator with a higher clamp (`2000`) and only piles unhandled mail up to `inboxCap()*3` to bound memory. ### `inboxCap()` — and the post-mortem ```js function inboxCap(){ // base buffer gives a new player ~20s of grace before the flood overwhelms them; return 18 + lvl('cap')*UP_BY_ID.cap.add + perkAdd('serenity') + G.ascensions*0; } ``` Base capacity is **18**. The `cap` upgrade adds `+14` per level; the `serenity` perk adds `+10` per level permanently. > **DEV NOTE** — the post-mortem. The base cap was originally **8**, which is roughly what a real "manageable inbox" feels like. It was a disaster. On the Startup plane the flood plus the first ramp tick could overflow an 8-slot cap inside the first ten seconds, before a new player had read the help dialog or bought a single upgrade. The status bar screamed OVERWHELMED, income halved, and the player's first impression was a game punishing them for not yet knowing the rules. We rebalanced the base to 18 — about 20 seconds of grace at the opening rate — and re-tuned the `cap` upgrade's add from `+5` to `+14` so a single purchase is a *felt* relief, not a rounding error. The vestigial `+ G.ascensions*0` term is the scar: we briefly granted cap per ascension, decided perks should own that axis instead (`serenity`), and left the zeroed term as a deliberate marker of where the knob used to be. Don't delete it without reading this note — it's a comment that compiles. ### `isOverwhelmed()` and the penalty ```js function isOverwhelmed(){ return now() < G.overwhelmedUntil || G.inbox.length > inboxCap(); } function overwhelmPenalty(){ return isOverwhelmed() ? 0.5 : 1; } ``` Overwhelm is sticky. The instant `G.inbox.length` exceeds the cap, `step` sets `G.overwhelmedUntil = now()+1500` — a 1.5-second tail so that dropping to exactly cap doesn't flicker the status bar off and on every tick. While overwhelmed, `overwhelmPenalty()` returns `0.5` and is multiplied into *every* income path: `manualMult`, `ruleMult`, and through them every archive, reply, delete, and rule-banked dollar. > **DEV NOTE** — the wired-in fix. The original overwhelm mechanic *slowed arrivals* when you were drowning — which is exactly backwards. It made the game easier the worse you were doing, so the inbox self-corrected and the pressure evaporated. The comment in `step` is the fossil of that decision: `if(isOverwhelmed()) rate *= 1.0; // arrivals don't slow; YOUR income tanks instead`. The `*= 1.0` is a no-op on purpose, a tombstone marking where the bad multiplier was. The real penalty moved into `overwhelmPenalty()` and was threaded through the multiplier functions so it could never be forgotten at a call site — there is exactly one place to be overwhelmed and exactly one place it bites. The status bar copy ("⚠ OVERWHELMED — income halved until you dig out") promises this to the player, so the constant is a contract, not a free parameter. · · · ## Triage actions and the keyboard There are three manual actions, each operating on the selected mail. Productivity is banked through one of three multiplier helpers: ```js function manualMult(){ return plane().ppMult * Math.pow(UP_BY_ID.hotkeys.mult, lvl('hotkeys')) * perkMult('clarity') * overwhelmPenalty(); } ``` | Action | Function | Key | Payout | Notes | |--------|---------|-----|--------|-------| | Archive | `archiveSel` | `e` | `value × manualMult()` | `×readBonus()` if already read; `×0.85` if archived unread before the `reading` upgrade. | | Reply | `replySel` | `r` | `value × 1.5 × manualMult() × replyMult()` | Costs `replyCost()` Focus (floor 3, default 8). The only action VIPs reward. | | Delete | `deleteSel` | `#` / `Del` / `Backspace` | `(value×0.1 + 0.5) × manualMult()` | On a VIP instead: `−value×0.5×manualMult()`, clamped at 0, with a warning toast. | `reading` and Focus deserve a note. Archiving an *unread* mail "sight unseen" pays `×0.85` until you buy **Speed-Reading Course** (`reading`), after which reads pay a `readBonus()` premium instead. `archiveAllRead()` exploits this — it bulk-archives every read, non-VIP mail at full `readBonus`, which is the intended reward for the `j/k`-to-read-then-bulk-archive rhythm. Replies are Focus-gated: `replyCost()` currently floors at 3 but is wired to scale (the `8 - lvl('focusregen')*0.0` term is another zeroed knob awaiting a balance pass). The keyboard handler is the volume's tactile hook. It early-returns inside form fields (so typing a rule pattern doesn't archive mail), respects `Escape` to close modals, reserves `Ctrl/Cmd+S` for save, and otherwise dispatches: ```js switch(e.key){ case 'j': case 'ArrowDown': e.preventDefault(); moveSel(1); break; case 'k': case 'ArrowUp': e.preventDefault(); moveSel(-1); break; case 'e': e.preventDefault(); archiveSel(); break; case 'r': e.preventDefault(); replySel(); break; case '#': e.preventDefault(); deleteSel(); break; case 'Delete': case 'Backspace': e.preventDefault(); deleteSel(); break; } ``` A nice touch: if you're viewing a non-inbox folder and press a triage key, the handler snaps you back to the inbox first, so muscle memory never silently no-ops. `moveSel` marks mail read as it passes, which is what feeds `archiveAllRead`. The same actions are reachable from the toolbar, the Edit menu, and a right-click context menu — but the keyboard is the one that feels like a real mail client, and that fidelity *is* the design (see §5 of the manifesto: fidelity over flash). · · · ## The rules engine Rules are the automation layer — the moment the game stops being twitch triage and becomes systems-building. A rule record: ```js {field, op, pattern, action, enabled, hits} ``` | Field | Domain | Source | |-------|--------|--------| | `field` | `from`, `subject`, `address`, `type` | `RULE_FIELDS` | | `op` | `contains`, `matches`, `is` | `RULE_OPS` (`matches` is regex, gated behind an upgrade) | | `pattern` | string | user-entered | | `action` | `archive`, `delete`, `reply`, `flag` | `RULE_ACTIONS` | | `enabled` | bool | toggled by the checkbox in the manager | | `hits` | int | lifetime auto-triage count, shown per-rule and summed in the folder tree | ### Compilation `compileRule(r)` precompiles the matcher once, not per-mail. For `matches` it builds a case-insensitive `RegExp`; for `contains`/`is` it lowercases the pattern into `_needle`. A bad regex is caught and the rule is flagged `_bad` rather than allowed to throw inside the hot path: ```js function compileRule(r){ try{ if(r.op==='matches'){ r._re = new RegExp(r.pattern, 'i'); } else { r._re = null; r._needle = String(r.pattern).toLowerCase(); } r._bad = false; }catch(e){ r._bad = true; r._re=null; } } ``` `contains` is a `String.includes`; `is` is exact equality; `matches` runs the precompiled regex against the *raw* field value (the `i` flag handles case). The "glob/regex" distinction in practice is: without the `regex` upgrade you have substring and exact matching only, which covers most real filtering (`address contains spam.` → delete). The `regex` unlock turns on the `matches` operator and full pattern power — the builder even suggests `^(prize|winner|won)` as a starter. ### Application and slots ```js function applyRules(m){ const slots = ruleSlots(); const active = G.rules.filter(r=>r.enabled).slice(0, slots); for(const r of active){ if(ruleMatches(r, m)){ r.hits = (r.hits||0) + 1; autoTriage(m, r.action); return true; } } return false; } ``` Two things are doing real work here. First, **rule slots cap how many rules actually run** — `ruleSlots()` is `1 + lvl('ruleslots') + perkAdd('preset')`, and only the first `slots` *enabled* rules are evaluated. You can author more rules than you can run; the surplus sit dormant until you buy slots. Second, **first match wins** (the `return true`), so order is strategy — a broad `archive` rule above a specific `delete` rule will eat mail the delete rule wanted. `autoTriage` mirrors the manual payouts but banks through `ruleMult()` (which adds the `rulespeed` upgrade and the `enlightened` perk on top of plane and clarity): ```js function autoTriage(m, action){ let gain = 0; if(action==='archive'){ gain = m.value; folderInc('archive'); } else if(action==='delete'){ gain = m.value*0.1; folderInc('spam'); } else if(action==='reply'){ gain = m.value*1.4; folderInc('sent'); } else if(action==='flag'){ m.flagged=true; m._ruleHandled=false; G.inbox.push(m); return; } const banked = gain * ruleMult(); G.pp += banked; if(hasUp('autoresponder')) G.pp += banked * lvl('autoresponder')*UP_BY_ID.autoresponder.mult; trackThroughput(banked); } ``` `flag` is the odd one out: it doesn't bank or remove, it pushes the mail *into* the inbox with a highlight — the escape hatch for "auto-surface this VIP, don't auto-handle it." Everything else banks and is gone. The `autoresponder` upgrade skims a passive `12%`-per-level bonus on top of rule income, which is the idle payoff the spec promised. `addRule()` is the gatekeeper for authoring: it rejects empty patterns, refuses when `G.rules.length >= ruleSlots()`, blocks `matches` without the `regex` upgrade, charges **5 Focus** to author, and refunds that Focus if the regex turns out invalid. So even building automation costs the regenerating resource — you can't spam-author rules while drowning. > **DEV NOTE** — rules run in `simulateAway` too. Offline catch-up calls the exact same `applyRules` per spawned mail, which is the whole reason an idle inbox can stay near zero: your encoded judgment keeps working while the tab is closed. The welcome-back toast reports it honestly — "your Rules auto-banked **N PP**" — and warns if the inbox overflowed past cap while you were gone. If you ever change the arrival or rule math, change it in *one* place that both `step` and `simulateAway` call, or online and offline economies will silently diverge. They currently share `arrivalRate`, `genMail`, and `applyRules`; keep it that way. · · · ## Planes and prestige The five planes are the prestige ladder, each a multiplier on both volume and power: | Plane | `rate` | `ppMult` | Domain | |-------|-------|---------|--------| | Startup | 1 | 1 | `boringsoft.net` | | Enterprise | 6 | 7 | `enterprise-corp.com` | | Government | 32 | 55 | `agency.gov` | | The Whole Internet | 180 | 520 | `all-of.net` | | The Void | 1100 | 6400 | `∅.void` | Note `ppMult` outpaces `rate` at every step (7 vs 6, 55 vs 32, 520 vs 180, 6400 vs 1100). Ascension is a *net positive* on throughput — the volume terrifies, but the power compounds faster. That's the carrot that makes climbing feel good rather than punishing. ### The Inbox-Zero gate You cannot prestige on cooldown or by spending; you must **genuinely reach Inbox Zero**: ```js function canAscend(){ return G.inbox.length===0 && enlightenmentFor()>=1; } ``` With the flood always arriving, hitting exactly zero is a real achievement — you have to out-triage or out-rule the incoming rate long enough to drain the last mail, then immediately ascend before the next spawn. `enlightenmentFor()` is the prestige currency, sqrt-scaled off `peakThroughput` with a floor of 2000 and a plane bonus: ```js function enlightenmentFor(){ const p = G.peakThroughput; if(p < 2000) return 0; return Math.floor(Math.pow(p/2000, 0.42) * (G.plane+1)); } ``` `doAscend()` banks the Enlightenment, increments `plane` and `ascensions`, and resets the run: `pp`, `inbox`, `peakThroughput`, `spawnAccum`, `overwhelmedUntil`, and `folderCounts` all clear; Focus refills to max. Crucially it **keeps** perks and Enlightenment, and carries `perkAdd('preset')` rules forward — the **Inherited Filters** perk — re-stamping them with `hits:0` and recompiling. Your judgment survives the reincarnation even though everything else burns. ### Serenity perks (Enlightenment) The five permanent perks, bought in the Ascend dialog with Enlightenment, persist across every run: | Perk | Effect per level | Max | |------|-----------------|-----| | `clarity` | ×1.30 global Productivity | 50 | | `serenity` | +10 base Inbox cap | 50 | | `flow` | +0.4 Focus regen | 40 | | `preset` | +1 carried-over rule slot per ascension | 20 | | `enlightened` | ×1.25 Productivity from *rules* (compounds with `rulespeed`) | 40 | The endgame is reaching Inbox Zero on **The Void** — the secret ending — where the mail rate is 1100× and the only thing that empties the inbox is an automation stack so complete it triages the entire internet faster than it arrives. The Ascend dialog acknowledges it: *"You are on The Void — the highest plane. Reaching Zero here is the secret ending."* Thematically it rhymes with every other volume's terminus — **Vol I — qBitTorrz**'s Singularity, **Vol II — DEFRAG.EXE**'s migrated platter — the substrate grown so large it's indistinguishable from no substrate at all. · · · ## Save-key registry and state shape `freshState()` seeds the global `G`. The persistence-relevant keys: | Key | Default | Role | |-----|--------|------| | `pp` | 0 | Productivity (the spendable run currency) | | `focus` / `focusMax` | 30 / 30 | Regenerating pool for Replies & rule authoring | | `enlightenment` | 0 | Prestige currency (persists) | | `peakThroughput` | 0 | Best PP/s this run; drives `enlightenmentFor` and the ramp | | `plane` / `ascensions` | 0 / 0 | Prestige depth | | `inbox` | `[]` | Live mail array; the survival gauge's numerator | | `sel` / `folder` | `null` / `inbox` | Selection and active folder | | `folderCounts` | `{archive,spam,sent}` | Lifetime-this-plane processed counters | | `upgrades` / `perks` | `{}` / `{}` | Level maps, keyed by id | | `rules` | `[]` | Rule records (sans the recompiled `_re`/`_needle`) | | `overwhelmedUntil` | 0 | Sticky-overwhelm timestamp | | `spawnAccum` | 0 | Fractional-arrival accumulator | `sanitizeState()` is the guard that makes a corrupt save un-brickable, per the shared contract. It coerces every numeric to a finite default, clamps `plane` into `[0, PLANES.length-1]`, drops unknown upgrade/perk ids, rebuilds the inbox array element-by-element (defaulting unknown `type`s to `work`), and — importantly — **revalidates every rule against `RULE_FIELDS`/`RULE_OPS`/`RULE_ACTIONS` and recompiles it**, so a save edited to contain a malicious or malformed regex can't execute as anything but a flagged-`_bad` no-op. It also reconciles `_mid` past the highest surviving id so new mail never collides with loaded mail. · · · ## If you change this - **`inboxCap()` base (18).** This is a tuned grace window, not a free number — read the cap post-mortem note above. Lowering it reintroduces the open-second overwhelm that made new players feel punished. If you change it, re-tune the `cap` upgrade's `add` (14) in the same pass. - **The overwhelm penalty.** `overwhelmPenalty()` returns `0.5` and the status bar literally promises "income halved." It is wired through `manualMult`, `ruleMult`, and `autoTriage`. If you change the factor, change the UI copy; if you add a new income path, route it through a multiplier helper, not a raw `G.pp +=`. Do **not** resurrect the arrivals-slow-down mechanic — the `*= 1.0` tombstone in `step` exists to stop exactly that. - **`arrivalRate()` and the ramp.** Online (`step`) and offline (`simulateAway`) must call the same rate, generator, and rule code or the two economies diverge. The `0.8` base and the `log10×0.18`, cap-8 ramp are balanced against the plane `rate` table — bumping one without the other breaks the climb. - **`value` is base, not banked.** Never bake a multiplier into a stored `value`, or post-ascension mail underpays. Apply multipliers at triage time, always. - **First-match-wins + slot truncation.** `applyRules` evaluates only the first `ruleSlots()` enabled rules and stops on first match. If you make rules run in parallel or all-match, you change the core strategy (rule ordering) and must rewrite the manager's "Active rules run top-to-bottom; first match wins" copy. - **The zeroed knobs (`G.ascensions*0`, `lvl('focusregen')*0.0`).** These are intentional markers of retired or reserved balance axes. Don't prune them as dead code without reading why they're zero — they document where a tuning lever used to live, or will.