docs(review): row 45 round 2 review record, approve (darkwing)

Comment 26884 on #1527, candidate 5b067a9d, queue rev 217.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-09 09:20:38 -05:00
co-authored by Claude Opus 5.5
parent 354fa9f6bb
commit b13fef4c28
33 changed files with 1692 additions and 0 deletions
@@ -0,0 +1,75 @@
✔ launch identity is stamped, payload identity is refused and stale holder cannot send (162.015698ms)
✔ decision classes route from policy; gated resolution is human-only, choice and target must match (238.937695ms)
✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (226.507639ms)
✔ launch events require a human CLI capability; generic emit cannot forge authority events (145.199058ms)
✔ within-role decisions close atomically and invalid options or blocking omissions refuse (139.010311ms)
✔ observer capabilities read human inbox but cannot mutate or forge launch identity (108.092373ms)
✔ task action subjects and linked decision trail are complete and ordered (142.820074ms)
✔ launch binding is durable and reconnecting requires the identical trusted record (89.92199ms)
✔ business isolation includes inherited object names and cross-business message references (148.898006ms)
✔ authority never transfers between action, run, target, unresolved or replaced role holder (217.258457ms)
✔ task projection uses schema current view, skipping earlier and equal-start polls (97.466084ms)
✔ revocation permanently bars the old run from reclaiming first, including after broker restart (164.013125ms)
✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (99.190667ms)
✔ both arbiters require human resolution when their cross-role route is themselves (159.018969ms)
✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.7987ms)
✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.377177ms)
✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (11.178996ms)
✔ expiry refuses use and env references never become client data (2.938494ms)
✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.242134ms)
✔ opaque tokens shorter than 16 characters refuse before use (0.255707ms)
✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.4647ms)
✔ process reader gets own kernel identity without exposing environment values (0.536978ms)
✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.672087ms)
✔ real pid 1 remains inspectable when its environment is protected (0.272176ms)
✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (192.11023ms)
✔ missing and foreign-business citations refuse and roll back message and grant (166.47203ms)
✔ cross-role sends still need a matching resolved decision and consume it once (221.690021ms)
✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (173.461124ms)
✔ startup token refusal returns safe code without value or partial listening broker (40.501746ms)
✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (158.580375ms)
✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (160.171734ms)
✔ trusted host registers later launches; socket clients never have a registration verb (162.238268ms)
✔ runtime excludes declared project roots even when host supplies no repoRoots (37.813864ms)
✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (140.568512ms)
✔ v3b prototype refusals, views and append-only mutations (913.050256ms)
✔ gated approval authorizes once, survives store reopen, and fresh approval works (229.390347ms)
✔ another run cannot consume an approval; a failed check leaves it usable (191.19976ms)
✔ two scheduled callers have exactly one grant and one consumed refusal (135.096325ms)
✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (260.933043ms)
✔ class drift gated to cross-role refuses before consumption (152.990224ms)
✔ class drift cross-role to gated refuses before consumption (176.624868ms)
✔ class drift gated to within-role refuses before consumption (170.263951ms)
✔ class drift cross-role to within-role refuses before consumption (171.751209ms)
✔ class drift within-role to gated refuses before consumption (157.946592ms)
✔ class drift within-role to cross-role refuses before consumption (146.405394ms)
✔ message.send consumes approval and prevents a later send or authorize (161.944811ms)
✔ role.revoke consumes approval and prevents a later revoke or authorize (177.732221ms)
✔ creates private WAL store and excludes a second writer until explicit close (108.828781ms)
✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (164.292379ms)
✔ existing empty database and symlink runtime directory refuse, never initialize over damage (170.389631ms)
✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (141.000448ms)
✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (92.582353ms)
✔ async transactions refuse before invoking their function (78.895339ms)
✔ recordTask keeps sync reads and a role write apart (160.637571ms)
✔ read_at must be one canonical UTC format, so the projection compares strings safely (97.161259ms)
✔ a bad entry refuses the whole record (90.241337ms)
✔ taskView reads the projection for one business (120.609459ms)
✔ requestTask hands only a holder and a task verb to the handler, and records refusals (209.339689ms)
✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (377.120673ms)
✔ without an adapter the server refuses every task verb (172.529691ms)
✔ the runtime refuses an invalid adapter and closes a valid one (175.430432ms)
✔ the process loads the S3 adapter from plain-data trackers (214.254327ms)
✔ socket capability stamps launch identity; shared views use wire, no SQL client (139.247766ms)
✔ two wire claims serialize; a lost reply never automatically retries (160.969295ms)
✔ malformed, oversized and identity-forging envelopes refuse without echoing input (94.867731ms)
✔ client preserves UTF-8 when a response divides a multibyte character (11.579482ms)
✔ committed mutation followed by dropped reply reports unknown and is never retried (123.680807ms)
ℹ tests 67
ℹ suites 0
ℹ pass 67
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2207.510046
@@ -0,0 +1,76 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (98.324231ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (120.368235ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (96.733383ms)
✔ decide prints a declining choice as declining (82.379171ms)
✔ an unknown outcome is reported once and never resent (84.806648ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (90.973543ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (87.575421ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (56.105972ms)
✔ every human command refuses inside an agent run before it touches the bus (81.207895ms)
✔ usage errors exit 4; no business and no host is a usage error (87.607774ms)
✔ agents and tasks print through the broker (86.794355ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.700549ms)
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (39.652275ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (35.782793ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.193332ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (28.274721ms)
✔ a business without tracker.baseUrl gets no trackers entry (28.329289ms)
✔ an unknown business and a broken system config refuse with exit 3 (55.285833ms)
✔ empty views say so (0.837733ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (1.22402ms)
✔ tasks print the tracker fields the snapshot carries (0.200943ms)
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (891.652263ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (211.576188ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (118.738702ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (157.896095ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (138.727897ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (106.451196ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (149.458737ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (101.246399ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (158.495929ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (22.143789ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.057949ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (210.363695ms)
✔ bus start refuses with exit 3 without a notifier config (85.767742ms)
✔ bus start runs until bus stop; status reports it while it runs (657.700101ms)
✔ bus-service.sh renders the unit and installs it into a given directory (26.582181ms)
✔ zoned uses the IANA zone across DST (17.121872ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (99.555481ms)
✔ two blocking decisions get two DMs with different nonces (108.930294ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.198262ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (99.005505ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (85.723331ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (96.358267ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (86.751728ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (145.049471ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (110.984735ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (104.664968ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (71.471682ms)
✔ an inbox read failure is logged and the next poll retries (0.644589ms)
✔ no Discord id reaches the journal or the log (60.432747ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (10.692664ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (22.745435ms)
✔ the journal: a whole file that is one torn line truncates to empty (12.160028ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.650783ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.576425ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.791869ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.508901ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.471877ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.370431ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.411411ms)
✔ digest content stays within Discord's 2000 characters (0.273224ms)
✔ runLoop never overlaps ticks and stops after the one in flight (110.798576ms)
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (349.853585ms)
✔ the transport writes {business, verb, args} to the child and reads its JSON (33.941143ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2150.665848ms)
✔ busExit and refuseInsideAgent (0.47624ms)
ℹ tests 66
ℹ suites 0
ℹ pass 66
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3338.283638
@@ -0,0 +1,186 @@
✔ approvals: a request is validated before anything is posted; the rendering shows names and never ids (3.319218ms)
✔ approvals: the ledger is appended and folded into open requests with bind and approval states (10.341889ms)
✔ approvals: a reply approves only when it points at a request, says exactly approve, and comes from a listed approver once (0.726016ms)
✔ approvals: a button approves only on its own request message with the matching custom id (0.523111ms)
✔ approvals flow: a turn that opened a request posts the message with the button, records it, binds it, and both approvers approve (26.751513ms)
✔ approvals flow: a non-approver, a repeat, a wrong custom id and a service refusal each get their fixed line and a drop entry (8.335326ms)
✔ approvals flow: an invalid request from the model, a refused post, and no api client are recorded and post nothing (7.905778ms)
✔ approvals flow: start retries a bind and an approval left as unknown, under their original keys (2.501545ms)
✔ authorize: open channel, listed user (2.129233ms)
✔ authorize: wrong guild (0.209397ms)
✔ authorize: no guild (DM) (0.17105ms)
✔ authorize: unlisted channel (0.190673ms)
✔ authorize: unknown channel, no info (0.244211ms)
✔ authorize: thread of listed parent (0.186345ms)
✔ authorize: thread of unlisted parent (0.264489ms)
✔ authorize: text channel that is not a thread and not listed (0.157821ms)
✔ authorize: unlisted user (0.940363ms)
✔ authorize: no author (0.543602ms)
✔ authorize: bot author (listed id, bot flag) (0.24993ms)
✔ authorize: system author (0.304058ms)
✔ authorize: the bot itself (0.117909ms)
✔ authorize: webhook (0.097568ms)
✔ authorize: mention channel without mention (0.136286ms)
✔ authorize: mention channel with bot mention (0.141821ms)
✔ authorize: mention channel with @everyone only (0.300636ms)
✔ authorize: mention channel mentioning someone else (0.096957ms)
✔ authorize: mention channel, content says @bot but mentions empty (0.110415ms)
✔ authorize: private thread under mention channel, mentioned (0.120272ms)
✔ authorize: private thread under mention channel, not mentioned (0.079172ms)
✔ authorize: thread in another guild per channel info (0.07309ms)
✔ authorize: not an object (0.066659ms)
✔ authorize: no id (0.064212ms)
✔ authorize: oversize content is accepted and flagged (0.070187ms)
✔ authorize: exactly the limit is not oversize (0.064804ms)
✔ authorize: a user's channel allowlist drops them outside it, threads count as the parent, others are unaffected (0.52028ms)
✔ authorize: order puts wrong guild before user, and user before channel (no channel lookup for strangers) (0.159049ms)
✔ binding: a complete binding validates and is frozen (3.068586ms)
✔ binding: unknown key, missing field, wrong type refuse with exit 2 (1.388934ms)
✔ binding: empty allowlists refuse (0.391764ms)
✔ binding: a user's channel allowlist must be non-empty, listed and unique; absent means every listed channel (1.155231ms)
✔ reloadDiff: reloadable keys are summarised by id; every fixed key refuses with exit 2 (1.587279ms)
✔ binding: dmRecipient is optional, a snowflake, one of the listed users, and a fixed key (1.209747ms)
✔ binding: file must be 0600, regular, not a symlink (1.974052ms)
✔ binding: token file mode, symlink, emptiness and shape are checked; token never appears in errors (2.013453ms)
✔ cli: check refuses a non-0600 token file with exit 2 before any network use (88.069044ms)
✔ context files: absolute paths, traversal, symlinks and out-of-repo targets refuse; in-repo files resolve (1.93664ms)
✔ cli: check refuses a missing context file and a missing binding with exit 2; usage is exit 4 (294.845783ms)
✔ cli: reload validates the file first (exit 2), then needs a live owner (exit 1); usage is exit 4 (199.288899ms)
✔ cli: run refuses when STOP is present, before any network use (127.966382ms)
✔ binding: tools is optional, validated strictly, a fixed key for reload, and its roots are resolved against the data root (1.89722ms)
✔ binding: a git key is validated at load and reaches the extension whole, and only on a writable root (1.434949ms)
✔ delivery: an accepted message is in the inbox before the turn, the reply is chunked with one nonce per chunk, and the turn record is write-once (23.024268ms)
✔ delivery: refused and unknown outcomes are journaled; a later chunk is not sent after a failure (17.482703ms)
✔ delivery: restart with an unknown entry re-sends the same nonce once and reconciles before accepting traffic (1.579395ms)
✔ delivery: an unknown entry older than the dedupe window is marked refused, not re-sent; a still-unknown one refuses start (1.112008ms)
✔ delivery: repeated unknown reconciliations never refresh the dedupe window; the original intent time decides (1.396954ms)
✔ turn: a failed engine turn posts the fixed line, never model output, and writes a failed record (3.112956ms)
✔ turn: a second message during a turn is held by the engine, both get their own reply and record (31.945738ms)
✔ turn: a thread under a listed channel is answered in the thread; an unknown thread is looked up once (3.216269ms)
✔ drop: an unlisted user gets silence and one drop line; no inbox entry, no REST call, no engine call (1.229429ms)
✔ drop: an oversize message is accepted into the inbox, answered with the fixed line and journaled as a drop (0.81815ms)
✔ restart: an inbox with three ids and a replay of the same three produces zero turns (40.682059ms)
✔ stop: STOP present refuses start; STOP written while running refuses new turns and the current one finishes (31.989003ms)
✔ ceiling: the ceiling plus one is refused and journaled; one fixed line per UTC day; a new day accepts again (6.138281ms)
✔ ceiling: a burst arriving while turns are still running cannot queue past the ceiling (3.763872ms)
✔ ceiling: a turn interrupted by a crash still counts after restart; admissions are durable (2.282296ms)
✔ ceiling: the daily notice survives a same-day restart; one delivery attempt in total, even when the first attempt crashed mid-flight (4.011565ms)
✔ duplicate: the same event delivered twice while the thread lookup is held yields one prompt, one admission and one reply (2.230563ms)
✔ journal: no token-shaped string and no model output on the drop path reaches disk (0.459549ms)
✔ receipt: an admitted message gets one eyes reaction on the inbound message; drops and refusals get none; a failed reaction is recorded and does not fail the turn (2.530831ms)
✔ receipt: Discord refusing the reaction leaves the turn intact and records ok false (1.848736ms)
✔ reload: a new user is silent before and answered after; a removed channel goes silent; a lower ceiling applies at once (3.468681ms)
✔ reload: a fixed key refuses with exit 2 and the old binding stays in force (1.901839ms)
✔ tools: with a tools binding the turn record lists every read and its outcome; without one the field is null (4.590872ms)
✔ context: the Discord block names the server, channels and modes, and states the rules from Q15 and Q16 (2.625169ms)
✔ context: with tools the block names the roots, keeps file content as data, and says to state refusals plainly (1.674023ms)
✔ context: a writable root adds the write rules and says a write is real only once Jason commits (2.102774ms)
✔ context: the envelope is one bracketed line then the text; names cannot break the line (1.258674ms)
✔ context: a git root swaps the terminal-commit line for the git verbs, and a vault root adds the id protocol (1.470798ms)
✔ context: assembleContext concatenates files in launcher format and appends the block; sha256 is stable (1.532689ms)
✔ context: splitReply keeps paragraphs together under the limit and splits long ones at lines, spaces, then hard (0.874631ms)
✔ engine: buildPiArgs carries the fixed flags, engine settings, session dir and prompt file (2.768139ms)
✔ engine: with tools, buildPiArgs turns pi's own tools off, loads the extension explicitly and allowlists exactly our three (0.441234ms)
✔ engine: a run with tool turns settles once, on the answer, with every tool call in the result (42.414407ms)
✔ engine: a run that ends on a tool-only turn fails the prompt as empty; a retried run settles on the real end (33.957373ms)
✔ engine: one prompt, one turn, text and usage come back (29.430695ms)
✔ engine: a prompt while streaming is held until pi settles, then sent as its own run, and answered in order (332.244955ms)
✔ engine: a held prompt that times out before pi settles fails on its own and is never sent (238.394961ms)
✔ engine: timeout sends abort and fails only that turn; the process stays (105.376236ms)
✔ engine: tool events from a run that outlived its timeout never land in the next prompt's record (229.347905ms)
✔ engine: a prompt after a turn that timed out before its agent_start waits for pi to settle instead of being refused (136.426007ms)
✔ engine: when pi has not started a timed-out turn by the end of the abort grace, the engine stops pi and fails held prompts (214.01799ms)
✔ engine: a timed-out turn pi starts only after the grace never answers a later prompt (614.727805ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (early prompt response) (1.358632ms)
✔ engine: late events of a run past its grace, before pi exits, answer nothing and nothing more is sent (late prompt response) (0.451687ms)
✔ engine: a timed-out run pi did start outlives the grace; the next prompt goes out when it ends (428.877802ms)
✔ engine: a malformed JSONL line fails the turn, not the process (28.369789ms)
✔ engine: a turn that ends in error rejects with the error code; process exit fails pending turns (43.555174ms)
✔ gateway: hello -> identify with intents, ready, heartbeat with jitter, ack (3.422037ms)
✔ gateway: missed ack closes the socket and resumes with the last sequence (1.702045ms)
✔ gateway: op 7 reconnect resumes; op 9 non-resumable re-identifies (0.689627ms)
✔ gateway: op 9 resumable resumes (0.343438ms)
✔ gateway: close 4014 is fatal, reports the missing intent, never reconnects (0.738513ms)
✔ gateway: 4004 and 4013 are fatal too; 1006 reconnects with identify when no session (0.49812ms)
✔ gateway: close() is final and unparseable frames are ignored (0.557496ms)
✔ git: config validation is strict, needs write: true, a work tree and a private token file (54.06605ms)
✔ git: the child environment drops every host git config, names one helper, and carries the token path only for origin (38.03988ms)
✔ git: status reports the branch, ahead/behind and changed paths, and refuses off the named branch or mid-merge (65.779339ms)
✔ git: parseStatus reads porcelain v2 including renames and conflicts (0.270821ms)
✔ git: a commit stages exactly the named files, carries the seat author and the requester trailer, and pushes at once (77.363486ms)
✔ git: commit refusals: message, paths, requester, nothing to commit, and an index that already holds other work (87.415844ms)
✔ git: a commit whose push fails is still a commit, says so, and the next commit's push carries both (D6) (86.215014ms)
✔ git: pull is fast-forward only; a diverged origin or dirty local files refuse with nothing merged (208.76542ms)
✔ git: push pushes the named branch only and reports up to date (70.945851ms)
✔ git: no token value or token path ever reaches a git argument list; outputs are masked and capped (102.837324ms)
✔ git: the credential helper answers get over https from a private file and nothing else (206.730441ms)
✔ git: the vault protocol validates before a commit, honours another owner's lock, reserves ids, and locks around writes (731.848737ms)
✔ lock: the claim is exclusive; a second start against a live owner refuses (5.177512ms)
✔ lock: a stale lock (dead owner, reused pid, or record without start) refuses run and is never signaled; only unlock clears it (73.644698ms)
✔ lock: an incomplete claim (directory without owner record) is busy and refuses run; unlock clears it (0.748285ms)
✔ lock: an owner record that exists but cannot be read is invalid: never signaled, never removed, never claimed over (2.846541ms)
✔ lock: legacy upgrade; a live connector holding a {pid, start} record is unknown, unlock refuses and nothing changes; after it exits, unlock clears it (35.63151ms)
✔ lock: a live pid whose record carries a malformed or noncanonical start or boot string is unknown, not a mismatch; nothing signals, removes, or claims over it (292.352316ms)
✔ lock: identity syntax; only canonical unsigned decimal start ticks and lowercase boot uuids are identities (0.44681ms)
✔ lock: a process whose start marker or boot id cannot be read refuses to claim (22.79696ms)
✔ lock: a live pid whose identity cannot be read right now is unknown: never signaled, never removed, never claimed over (0.994977ms)
✔ lock: four processes racing for the same binding; exactly one claims it and the others refuse (40.307438ms)
✔ lock: stale handoff; concurrent starts over a stale lock all refuse, nothing reclaims, one unlock then exactly one live owner (142.645709ms)
✔ lock: four-party schedule; claims landing inside an unlock's gap never survive, one unlock leaves no owner and no residue (95.799317ms)
✔ notices: a kind is recorded per UTC day and found again (0.483637ms)
✔ notify: the DM channel opens once, every send carries the nonce, and only the message id comes back (5.398814ms)
✔ notify: refusals and unknowns surface as RestOutcome without ids; a refused send reopens the channel next time (2.385403ms)
✔ notify: no dmRecipient, a non-0600 binding or token, or a missing binding refuse before any network use (1.743489ms)
✔ recover: nothing to do is clean; a lock whose owner is gone or that has no record is cleared and STOP ends up absent (42.8104ms)
✔ recover: an operator STOP refuses with exit 3 and is never removed, whatever the lock says (30.081535ms)
✔ recover: a brake written during the unlock wins; STOP stays with both lines and the start is refused (26.370058ms)
✔ recover: a held binding refuses with exit 3 and writes no STOP: live owner, alive pid without verifiable identity, unreadable record (67.302474ms)
✔ cli: recover exits 0 when ready, 3 behind a brake or a held binding, and run's own STOP refusal is 3 (670.400393ms)
✔ rest: createMessage sends nonce, enforce_nonce, empty allowed_mentions and a soft reply reference (3.699245ms)
✔ rest: 429 waits retry_after and retries; 4xx is refused; 5xx and socket errors are unknown (3.462425ms)
✔ rest: content and nonce limits are enforced locally; typing never throws (0.672306ms)
✔ rest: react PUTs the encoded emoji on the inbound message for @me; 2xx is true, anything else is false and never throws (1.361755ms)
✔ rest: createDm posts the recipient and resolves the channel id; 4xx refused, 5xx unknown, bad id never sent (1.094646ms)
✔ setspark config: a bare https or loopback origin, a private key file, a principal (5.496834ms)
✔ setspark config: reaches the tools config and the binding as a fixed key (2.793536ms)
✔ setspark config: the binding's key survives resolveToolRoots and the engine's JSON hand-off to the extension (1.59691ms)
✔ setspark config: approvers come from the binding's users, never from the binding's setspark key (2.130609ms)
✔ setspark verbs: required_approvers go out as discord ids from names and come back as names (20.667767ms)
✔ setspark verbs: no Discord user id reaches tool text, whatever shape the service returns it in (9.116801ms)
✔ setspark contract: a decision made with names opens a request the connector accepts; names stored by an old record still refuse (7.498466ms)
✔ setspark keys: read per call, one printable token per file, rotation without a restart (3.563633ms)
✔ setspark idempotency keys: principal, turn id, call index; connector keys name a step (0.602942ms)
✔ setspark http core: json in and out, bearer header, idempotency header, fixed user agent, no key anywhere else (2.626814ms)
✔ setspark http core: error bodies become fixed refusals with code and the 409 fields; server text is data, cut (1011.335094ms)
✔ setspark verbs: a setspark key enables the eight verbs and no counters (0.435874ms)
✔ setspark verbs: writes carry the turn's key and the asserted requester, reads carry no key, and the api key never appears in text or details (6.088849ms)
✔ setspark verbs: no turn refuses every write before any request; bad arguments refuse before any request; reads still work (1.355094ms)
✔ setspark verbs: renderRecord caps long output and hides the accepted snapshot (0.236364ms)
✔ setspark api: bind, add_approval (button and reply) and get use integer request ids and the connector's keys (3.70358ms)
✔ tools: config refuses a missing, symlinked, dotted, non-directory or duplicate root and bad limits (5.188167ms)
✔ tools: every escape is refused with a fixed reason and nothing outside the root is read (4.122393ms)
✔ tools: happy paths list, read a window, and search case-insensitively; dotfiles and symlinks never appear (5.24716ms)
✔ tools: the tool set renders text for the model, records details for the journal, and enforces the per-run budget (3.611039ms)
✔ tools: listing and search caps hold (8.486574ms)
✔ tools: credential shapes are caught; ordinary prose and ids are not (0.853503ms)
✔ tools: the read uses the checked file itself; a symlink, a swapped file, a FIFO, a grown file or a hard link at read time is refused (5.282273ms)
✔ tools: an unreadable file under the root is skipped by search and refused by read (1.213689ms)
✔ tools: config accepts write: true only as a boolean, and enables the write tools only then (1.334094ms)
✔ tools: every write outside the fence is refused before any byte lands, and no temp file remains (4.316392ms)
✔ tools: write_file leaves the exact bytes, edit_file replaces one exact match, and the set renders the change as uncommitted (2.392391ms)
✔ tools: a target that changed between the check and the rename is refused and the temp file is removed (1.278135ms)
✔ web: config takes an https or loopback-http SearXNG base url and a bounded fetch cap (3.565372ms)
✔ web: address rules refuse every private, loopback, link-local, mapped and multicast form (2.387745ms)
✔ web: web_fetch refuses bad urls, private hosts, rebinding names, non-https redirects, too many hops, error status, non-text bodies, and times out (1026.320493ms)
✔ web: web_fetch returns html as text with the title, follows an https redirect, keeps plain text and json, and cuts at the cap (4.455325ms)
✔ web: html to text drops scripts, styles and comments, decodes entities and keeps block breaks (0.242803ms)
✔ web: web_search asks the instance for json, returns at most ten clean results, and refuses a bad query, a down instance or an unusable answer (2.759567ms)
✔ web: the tool set enables the web tools only with a web key, counts them in the budget, and records url, status and hits (2.461068ms)
ℹ tests 178
ℹ suites 0
ℹ pass 178
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2623.767118
@@ -0,0 +1,59 @@
✔ the boot config is checked before anything starts (88.370383ms)
✔ a business with no tracker entry refuses task verbs (109.95431ms)
✔ credential.expiring and .expired are recorded once per instance (196.285908ms)
✔ a token file that changes on disk records credential.changed (98.992495ms)
✔ autostart polls, reconciles and retries a startup the tracker was down for (116.522176ms)
✔ a refusal a restart must clear is not retried by the poll (68.938902ms)
✔ a poll that fires while two are queued is dropped (80.786377ms)
✔ close waits for a running verb and refuses one that has not started (193.083833ms)
✔ the bundled Vikunja is the pinned upstream image the runbook names (0.761291ms)
✔ every published port is on 127.0.0.1, and no secret is in the file (0.270456ms)
✔ the fake answers each route with the statuses and shapes Vikunja v2.7.0 sent (334.579482ms)
✔ the recorded task bodies pass the checks S3 applies to every read (0.527552ms)
✔ the client works against the fake over real HTTP with the platform fetch (224.546898ms)
✔ a correct install starts, and the first reconcile records tasks that already exist (93.611883ms)
✔ verbs refuse while a business is starting and after startup refused it (120.776979ms)
✔ startup refuses a token that can do more than its role needs (332.454022ms)
✔ startup refuses an unsupported version and flags an untested one (223.024049ms)
✔ startup refuses a board that the runbook did not install (333.513824ms)
✔ startup refuses a project the sync bot cannot read (69.336789ms)
✔ startup refuses a configured label the pm bot cannot see (60.319645ms)
✔ startup refuses an expired credential and a missing sync credential (158.510591ms)
✔ an unreachable tracker refuses with tracker-unavailable (87.544085ms)
✔ an edit in the UI is recorded once, with the fields that changed (183.880792ms)
✔ a move between open buckets is seen on the board, though updated does not change (163.615214ms)
✔ a person's comment is counted and a bot's is not (239.561847ms)
✔ the hourly reconcile catches a comment through comment_count (205.68284ms)
✔ a task closed in the UI leaves the open view with its done bucket (376.737742ms)
✔ a task that leaves the board is recorded as deleted, moved or out of reach (239.781275ms)
✔ a poll that read before a verb wrote does not overwrite the verb (162.559594ms)
✔ a tracker fault during a tick is reported and the next tick catches up (152.896094ms)
✔ a malformed answer refuses the tick with tracker-shape (126.29894ms)
✔ no token value reaches the database, the log or a refusal (251.223863ms)
✔ the first look at a task counts only comments inside the window (147.694916ms)
✔ task.create needs a recorded human request and a requirement id (183.457536ms)
✔ only labels named in the business file can be written (201.788761ms)
✔ task.schedule sets and clears a due date and relations (256.919575ms)
✔ assign and reassign move the role bots and record task.assigned (275.339038ms)
✔ task.update.assigned is for the assignee and records task.state (273.373553ms)
✔ a wrong expected digest records task.conflict and writes nothing (186.357206ms)
✔ a cross-role verb needs a resolved decision, used once (217.013242ms)
✔ task.close needs a verdict; after it every verb refuses with task-done (207.408928ms)
✔ a lost answer is settled by a re-read and never retried (234.351525ms)
✔ a create whose answer is lost is reported uncertain, and the poll finds the task (169.38299ms)
✔ a task the sync bot cannot read refuses and records nothing (116.75312ms)
✔ verbs and polls for one business run one at a time (219.459289ms)
✔ a due date with milliseconds is written to the second (164.713733ms)
✔ every write landed and the final read failed: the verb succeeds and records what it wrote (110.892235ms)
✔ some writes landed and the final read failed: write-uncertain, and nothing is recorded (74.56379ms)
✔ a create whose final read fails succeeds and records task.created (106.058791ms)
✔ an edit between the last write and the final read shows as external on the next poll (110.121599ms)
✔ task.created is recorded when a later label write fails (86.010496ms)
ℹ tests 51
ℹ suites 0
ℹ pass 51
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3287.736661
@@ -0,0 +1,5 @@
node cli rc=0 ℹ tests 66 ℹ pass 66 ℹ fail 0
node discord rc=0 ℹ tests 178 ℹ pass 178 ℹ fail 0
node bus rc=0 ℹ tests 67 ℹ pass 67 ℹ fail 0
node tasks rc=0 ℹ tests 51 ℹ pass 51 ℹ fail 0
test-discord rc=0 OK service install with USER unset finishes and names the account for lingering discord suite: 66 passed, 0 failed
@@ -0,0 +1,70 @@
toolchain: node v26.8.1
OK syntax: packages/discord/src/approvals.mjs
OK syntax: packages/discord/src/authorize.mjs
OK syntax: packages/discord/src/binding.mjs
OK syntax: packages/discord/src/cli.mjs
OK syntax: packages/discord/src/connector.mjs
OK syntax: packages/discord/src/context.mjs
OK syntax: packages/discord/src/engine-pi.mjs
OK syntax: packages/discord/src/errors.mjs
OK syntax: packages/discord/src/gateway.mjs
OK syntax: packages/discord/src/git.mjs
OK syntax: packages/discord/src/journal.mjs
OK syntax: packages/discord/src/notify.mjs
OK syntax: packages/discord/src/rest.mjs
OK syntax: packages/discord/src/setspark.mjs
OK syntax: packages/discord/src/tools.mjs
OK syntax: packages/discord/src/web.mjs
OK syntax: packages/discord/bin/git-credential.mjs
OK syntax: packages/discord/extension/tools.mjs
OK syntax: packages/discord/tests/approvals.test.mjs
OK syntax: packages/discord/tests/authorize.test.mjs
OK syntax: packages/discord/tests/binding.test.mjs
OK syntax: packages/discord/tests/connector.test.mjs
OK syntax: packages/discord/tests/context.test.mjs
OK syntax: packages/discord/tests/engine.test.mjs
OK syntax: packages/discord/tests/fake-pi.mjs
OK syntax: packages/discord/tests/gateway.test.mjs
OK syntax: packages/discord/tests/git.test.mjs
OK syntax: packages/discord/tests/helpers.mjs
OK syntax: packages/discord/tests/journal.test.mjs
OK syntax: packages/discord/tests/notify.test.mjs
OK syntax: packages/discord/tests/recover.test.mjs
OK syntax: packages/discord/tests/rest.test.mjs
OK syntax: packages/discord/tests/setspark.test.mjs
OK syntax: packages/discord/tests/tools.test.mjs
OK syntax: packages/discord/tests/web.test.mjs
OK syntax: packages/discord/fixtures/claim-worker.mjs
OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs
OK syntax: scripts/discord.sh
OK syntax: scripts/discord-service.sh
OK packages/discord declares no dependencies
OK no bot-token-shaped string in packages/discord
OK fixture binding uses placeholder ids only
OK fixture binding validates
OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool
OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start
OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root
OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token
OK real pi with protocol vault adds reserve_id to the git verbs
OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key
OK real pi refuses a git key on a read-only root (fail closed)
OK real pi with the pilot flags (--no-tools) exposes no tool at all
OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed)
OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT
OK node --test packages/discord/tests/ (ℹ pass 178)
OK scripts/discord.sh --help exits 0
OK scripts/discord.sh check without a binding exits 4
OK scripts/discord.sh recover without a binding exits 4
OK scripts/discord.sh reload without a binding exits 4
OK scripts/discord-service.sh without a command exits 4
OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP
OK service install writes the rendered unit (0644) and leaves no temp file
OK service install a second time reports unchanged
OK systemd-analyze verify accepts the rendered unit
OK service uninstall removes the unit file
OK service install with an unknown flag exits 4
OK service install with USER unset finishes and names the account for lingering
discord suite: 66 passed, 0 failed
@@ -0,0 +1,13 @@
poll 30 s; sends at minutes after the first: 0.0, 30.0, 60.0, 60.0, 60.5, 61.5, 63.5, 67.5, 75.5, 90.0, 91.5, 120.0
2026-10-09T12:00:00.000Z notify: dm refused (HTTP 403); retry in 1800 s
2026-10-09T12:30:00.000Z notify: dm refused (HTTP 403); retry in 1800 s
2026-10-09T13:00:00.000Z notify: dm refused (HTTP 403); retry in 1800 s
2026-10-09T13:00:00.000Z notify: digest refused (HTTP 403); retry in 30 s
2026-10-09T13:00:30.000Z notify: digest refused (HTTP 403); retry in 60 s
2026-10-09T13:01:30.000Z notify: digest refused (HTTP 403); retry in 120 s
2026-10-09T13:03:30.000Z notify: digest refused (HTTP 403); retry in 240 s
2026-10-09T13:07:30.000Z notify: digest refused (HTTP 403); retry in 480 s
2026-10-09T13:15:30.000Z notify: digest refused (HTTP 403); retry in 960 s
2026-10-09T13:30:00.000Z notify: dm refused (HTTP 403); retry in 1800 s
2026-10-09T13:31:30.000Z notify: digest refused (HTTP 403); retry in 1800 s
2026-10-09T14:00:00.000Z notify: dm dec-0001 refused 5 times; gave up, not retried
@@ -0,0 +1,62 @@
// Row 45 probe: the :144 window through startHost. Both children die at the
// same moment as the start send (as a cgroup-wide kill would do), so the
// broker can be dead while broker.connected is still true. Counts 'error'
// events on the broker and close sends; does not change host.mjs.
import { test } from "node:test";
import { readFileSync } from "node:fs";
import { subscribe, unsubscribe } from "node:diagnostics_channel";
import { bootConfig, loadSystem } from "/home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/src/config.mjs";
import { startHost } from "/home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/src/host.mjs";
import { makeDeployment } from "/home/jwoltje/darkwing-scratch/r45b/wt/packages/discord/tests/helpers.mjs";
import { fixture, tmp } from "/home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/tests/helpers.mjs";
const N = Number(process.env.N ?? 30);
const SIG = process.env.SIG ?? "SIGKILL";
const SPIN = Number(process.env.SPIN ?? 0); // ms of busy-wait between the two kills
const tally = { runs: 0, rejected: {}, closeSends: 0, errorEvents: {} };
for (let i = 0; i < N; i++) {
test(`window run ${i}`, async (t) => {
const root = tmp(t);
const f = fixture(root);
makeDeployment(root);
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
const children = [];
const onChild = ({ process: child }) => {
children.push(child);
const send = child.send;
};
subscribe("child_process", onChild);
t.after(() => unsubscribe("child_process", onChild));
let armed = true;
const origSends = [];
const startSpy = ({ process: child }) => {
let send;
Object.defineProperty(child, "send", { configurable: true, get: () => send, set(fn) {
send = function (m, ...rest) {
if (m?.op === "close") tally.closeSends++;
const r = fn.call(this, m, ...rest);
if (m?.op === "start" && armed) {
armed = false;
if (!process.env.NOLISTEN) children[0].on("error", (e) => (tally.errorEvents[e.code] = (tally.errorEvents[e.code] ?? 0) + 1));
children[0].kill(SIG);
const until = performance.now() + SPIN;
while (performance.now() < until);
// ZOMBIE: wait until the kernel has the broker dead (state Z), so its end of the channel is closed.
if (process.env.ZOMBIE) while (!/\) Z /.test(readFileSync("/proc/" + children[0].pid + "/stat", "utf8")));
children[1].kill(SIG);
}
return r;
};
} });
};
subscribe("child_process", startSpy);
t.after(() => unsubscribe("child_process", startSpy));
const started = startHost({ boot, business: "acme", notifier: { binding: "test-seat" }, log: () => {} });
const r = await started.then((h) => (h.close(0), "started"), (e) => `${e.exitCode}: ${e.message.replace(/\(\d+\)/, "(n)")}`);
await new Promise((r) => setTimeout(r, 20));
tally.runs++;
tally.rejected[r] = (tally.rejected[r] ?? 0) + 1;
});
}
test("tally", () => console.log("TALLY " + JSON.stringify({ SIG, SPIN, ...tally })));
@@ -0,0 +1,50 @@
✔ window run 0 (141.775913ms)
✔ window run 1 (128.801907ms)
✔ window run 2 (122.224765ms)
✔ window run 3 (120.55733ms)
✔ window run 4 (169.60705ms)
✔ window run 5 (131.958959ms)
✔ window run 6 (126.915052ms)
✔ window run 7 (114.99937ms)
✔ window run 8 (121.182822ms)
✔ window run 9 (120.432767ms)
✔ window run 10 (111.317391ms)
✔ window run 11 (118.376653ms)
✔ window run 12 (116.812163ms)
✔ window run 13 (117.915317ms)
✔ window run 14 (114.568821ms)
✔ window run 15 (120.844385ms)
✔ window run 16 (124.351157ms)
✔ window run 17 (246.938018ms)
✔ window run 18 (135.490176ms)
✔ window run 19 (135.996266ms)
✔ window run 20 (147.561527ms)
✔ window run 21 (147.364625ms)
✔ window run 22 (136.565185ms)
✔ window run 23 (130.609943ms)
✔ window run 24 (129.815264ms)
✔ window run 25 (118.789473ms)
✔ window run 26 (122.300832ms)
✔ window run 27 (121.1387ms)
✔ window run 28 (112.614013ms)
✔ window run 29 (119.452852ms)
✔ window run 30 (123.123535ms)
✔ window run 31 (125.832031ms)
✔ window run 32 (116.596912ms)
✔ window run 33 (121.209475ms)
✔ window run 34 (113.816661ms)
✔ window run 35 (114.378503ms)
✔ window run 36 (121.710931ms)
✔ window run 37 (130.852711ms)
✔ window run 38 (119.459241ms)
TALLY {"SIG":"SIGKILL","SPIN":1,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":34,"errorEvents":{}}
✔ window run 39 (122.578679ms)
✔ tally (0.364378ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5220.718135
@@ -0,0 +1,50 @@
✔ window run 0 (131.341481ms)
✔ window run 1 (128.087357ms)
✔ window run 2 (124.400352ms)
✔ window run 3 (128.290452ms)
✔ window run 4 (167.681471ms)
✔ window run 5 (172.17902ms)
✔ window run 6 (139.660295ms)
✔ window run 7 (123.332112ms)
✔ window run 8 (139.039813ms)
✔ window run 9 (154.833525ms)
✔ window run 10 (131.85663ms)
✔ window run 11 (141.022111ms)
✔ window run 12 (129.008786ms)
✔ window run 13 (129.266634ms)
✔ window run 14 (127.802054ms)
✔ window run 15 (128.21902ms)
✔ window run 16 (148.437164ms)
✔ window run 17 (146.149854ms)
✔ window run 18 (140.390096ms)
✔ window run 19 (130.447972ms)
✔ window run 20 (125.66318ms)
✔ window run 21 (127.645275ms)
✔ window run 22 (134.712369ms)
✔ window run 23 (212.831125ms)
✔ window run 24 (160.811058ms)
✔ window run 25 (151.876243ms)
✔ window run 26 (158.656402ms)
✔ window run 27 (151.846909ms)
✔ window run 28 (138.794384ms)
✔ window run 29 (156.56767ms)
✔ window run 30 (152.019386ms)
✔ window run 31 (152.23568ms)
✔ window run 32 (142.229226ms)
✔ window run 33 (150.701444ms)
✔ window run 34 (141.009523ms)
✔ window run 35 (129.71958ms)
✔ window run 36 (139.951769ms)
✔ window run 37 (148.138706ms)
✔ window run 38 (126.708619ms)
TALLY {"SIG":"SIGKILL","SPIN":1.2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":32,"errorEvents":{}}
✔ window run 39 (138.358993ms)
✔ tally (0.453262ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5791.811271
@@ -0,0 +1,50 @@
✔ window run 0 (134.461453ms)
✔ window run 1 (125.454162ms)
✔ window run 2 (120.844259ms)
✔ window run 3 (118.278859ms)
✔ window run 4 (119.420377ms)
✔ window run 5 (123.618231ms)
✔ window run 6 (114.292974ms)
✔ window run 7 (128.563253ms)
✔ window run 8 (116.479373ms)
✔ window run 9 (119.741072ms)
✔ window run 10 (121.673034ms)
✔ window run 11 (126.334584ms)
✔ window run 12 (121.757228ms)
✔ window run 13 (113.524238ms)
✔ window run 14 (116.188965ms)
✔ window run 15 (123.127899ms)
✔ window run 16 (118.355664ms)
✔ window run 17 (123.105738ms)
✔ window run 18 (112.473591ms)
✔ window run 19 (122.378022ms)
✔ window run 20 (121.221472ms)
✔ window run 21 (118.322399ms)
✔ window run 22 (129.595055ms)
✔ window run 23 (117.333743ms)
✔ window run 24 (125.865197ms)
✔ window run 25 (119.018576ms)
✔ window run 26 (126.665665ms)
✔ window run 27 (118.902827ms)
✔ window run 28 (118.759938ms)
✔ window run 29 (123.575605ms)
✔ window run 30 (134.11206ms)
✔ window run 31 (124.494316ms)
✔ window run 32 (174.835845ms)
✔ window run 33 (120.345857ms)
✔ window run 34 (115.834247ms)
✔ window run 35 (118.843564ms)
✔ window run 36 (125.738016ms)
✔ window run 37 (116.990457ms)
✔ window run 38 (118.406161ms)
TALLY {"SIG":"SIGKILL","SPIN":1.2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":16,"errorEvents":{}}
✔ window run 39 (121.674051ms)
✔ tally (0.929849ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4994.732641
@@ -0,0 +1,50 @@
✔ window run 0 (150.423142ms)
✔ window run 1 (129.678496ms)
✔ window run 2 (135.64539ms)
✔ window run 3 (134.545777ms)
✔ window run 4 (133.854188ms)
✔ window run 5 (131.422526ms)
✔ window run 6 (146.549266ms)
✔ window run 7 (125.651603ms)
✔ window run 8 (138.88489ms)
✔ window run 9 (138.303077ms)
✔ window run 10 (133.06485ms)
✔ window run 11 (135.114358ms)
✔ window run 12 (135.602968ms)
✔ window run 13 (138.54996ms)
✔ window run 14 (148.336899ms)
✔ window run 15 (129.236753ms)
✔ window run 16 (134.154075ms)
✔ window run 17 (142.141297ms)
✔ window run 18 (140.955466ms)
✔ window run 19 (150.086029ms)
✔ window run 20 (127.725663ms)
✔ window run 21 (134.892136ms)
✔ window run 22 (136.840094ms)
✔ window run 23 (134.386007ms)
✔ window run 24 (132.939513ms)
✔ window run 25 (149.985061ms)
✔ window run 26 (130.637847ms)
✔ window run 27 (126.481832ms)
✔ window run 28 (139.730464ms)
✔ window run 29 (138.867103ms)
✔ window run 30 (138.381503ms)
✔ window run 31 (132.513838ms)
✔ window run 32 (147.811325ms)
✔ window run 33 (134.918279ms)
✔ window run 34 (135.444945ms)
✔ window run 35 (140.115305ms)
✔ window run 36 (134.210217ms)
✔ window run 37 (132.593278ms)
✔ window run 38 (134.326022ms)
TALLY {"SIG":"SIGKILL","SPIN":1.4,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":24,"errorEvents":{}}
✔ window run 39 (157.539671ms)
✔ tally (0.357915ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5596.365423
@@ -0,0 +1,50 @@
✔ window run 0 (213.128355ms)
✔ window run 1 (153.415211ms)
✔ window run 2 (152.620388ms)
✔ window run 3 (148.341557ms)
✔ window run 4 (135.356542ms)
✔ window run 5 (153.319339ms)
✔ window run 6 (139.776851ms)
✔ window run 7 (139.392433ms)
✔ window run 8 (141.470615ms)
✔ window run 9 (127.012135ms)
✔ window run 10 (132.152329ms)
✔ window run 11 (129.520072ms)
✔ window run 12 (126.202089ms)
✔ window run 13 (128.173216ms)
✔ window run 14 (137.681637ms)
✔ window run 15 (139.430765ms)
✔ window run 16 (122.512087ms)
✔ window run 17 (129.919929ms)
✔ window run 18 (130.558413ms)
✔ window run 19 (131.36124ms)
✔ window run 20 (129.013316ms)
✔ window run 21 (166.617631ms)
✔ window run 22 (123.012037ms)
✔ window run 23 (135.309034ms)
✔ window run 24 (126.446534ms)
✔ window run 25 (122.134556ms)
✔ window run 26 (125.617635ms)
✔ window run 27 (128.164182ms)
✔ window run 28 (128.70883ms)
✔ window run 29 (122.974159ms)
✔ window run 30 (135.188099ms)
✔ window run 31 (130.358744ms)
✔ window run 32 (121.987698ms)
✔ window run 33 (123.236884ms)
✔ window run 34 (128.304914ms)
✔ window run 35 (124.938594ms)
✔ window run 36 (124.626017ms)
✔ window run 37 (132.164621ms)
✔ window run 38 (141.388114ms)
TALLY {"SIG":"SIGKILL","SPIN":1.5,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":17,"errorEvents":{}}
✔ window run 39 (155.341197ms)
✔ tally (0.317348ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5563.638746
@@ -0,0 +1,50 @@
✔ window run 0 (130.998528ms)
✔ window run 1 (119.704397ms)
✔ window run 2 (128.992131ms)
✔ window run 3 (120.791647ms)
✔ window run 4 (121.253779ms)
✔ window run 5 (121.657691ms)
✔ window run 6 (119.084951ms)
✔ window run 7 (120.267738ms)
✔ window run 8 (119.640471ms)
✔ window run 9 (145.253415ms)
✔ window run 10 (118.139956ms)
✔ window run 11 (124.894068ms)
✔ window run 12 (117.727296ms)
✔ window run 13 (136.557691ms)
✔ window run 14 (131.940352ms)
✔ window run 15 (119.838904ms)
✔ window run 16 (125.877707ms)
✔ window run 17 (120.973525ms)
✔ window run 18 (137.067272ms)
✔ window run 19 (130.935893ms)
✔ window run 20 (120.226148ms)
✔ window run 21 (125.321337ms)
✔ window run 22 (120.503036ms)
✔ window run 23 (120.708151ms)
✔ window run 24 (119.972725ms)
✔ window run 25 (122.504684ms)
✔ window run 26 (118.988665ms)
✔ window run 27 (118.134286ms)
✔ window run 28 (119.295177ms)
✔ window run 29 (123.745992ms)
✔ window run 30 (130.268251ms)
✔ window run 31 (122.333084ms)
✔ window run 32 (120.073336ms)
✔ window run 33 (121.821046ms)
✔ window run 34 (122.692467ms)
✔ window run 35 (118.482916ms)
✔ window run 36 (124.584566ms)
✔ window run 37 (116.446765ms)
✔ window run 38 (121.984162ms)
TALLY {"SIG":"SIGKILL","SPIN":1.8,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":3,"errorEvents":{}}
✔ window run 39 (124.246021ms)
✔ tally (0.320662ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5023.504777
@@ -0,0 +1,50 @@
✔ window run 0 (135.122939ms)
✔ window run 1 (130.830505ms)
✔ window run 2 (125.280209ms)
✔ window run 3 (128.350504ms)
✔ window run 4 (130.561417ms)
✔ window run 5 (116.417611ms)
✔ window run 6 (118.605832ms)
✔ window run 7 (121.551752ms)
✔ window run 8 (127.001897ms)
✔ window run 9 (122.214684ms)
✔ window run 10 (130.72462ms)
✔ window run 11 (129.924305ms)
✔ window run 12 (128.178601ms)
✔ window run 13 (121.399729ms)
✔ window run 14 (128.906791ms)
✔ window run 15 (131.373785ms)
✔ window run 16 (122.8896ms)
✔ window run 17 (123.958448ms)
✔ window run 18 (120.251958ms)
✔ window run 19 (121.732404ms)
✔ window run 20 (124.560213ms)
✔ window run 21 (117.381408ms)
✔ window run 22 (122.983482ms)
✔ window run 23 (119.628213ms)
✔ window run 24 (130.017879ms)
✔ window run 25 (144.728019ms)
✔ window run 26 (126.052281ms)
✔ window run 27 (131.412223ms)
✔ window run 28 (127.639003ms)
✔ window run 29 (131.003141ms)
✔ window run 30 (134.270388ms)
✔ window run 31 (136.042648ms)
✔ window run 32 (128.801888ms)
✔ window run 33 (129.968451ms)
✔ window run 34 (118.479997ms)
✔ window run 35 (137.586164ms)
✔ window run 36 (131.670247ms)
✔ window run 37 (153.289132ms)
✔ window run 38 (137.929792ms)
TALLY {"SIG":"SIGKILL","SPIN":1.2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":34,"undefined: write EPIPE":6},"closeSends":20,"errorEvents":{}}
✔ window run 39 (135.791186ms)
✔ tally (0.361776ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 23987.426252
@@ -0,0 +1,50 @@
✔ window run 0 (128.454236ms)
✔ window run 1 (142.346211ms)
✔ window run 2 (143.004179ms)
✔ window run 3 (122.139231ms)
✔ window run 4 (123.584734ms)
✔ window run 5 (122.762427ms)
✔ window run 6 (119.591144ms)
✔ window run 7 (119.323631ms)
✔ window run 8 (120.790452ms)
✔ window run 9 (117.146343ms)
✔ window run 10 (119.426328ms)
✔ window run 11 (114.21452ms)
✔ window run 12 (119.97472ms)
✔ window run 13 (122.748017ms)
✔ window run 14 (116.660207ms)
✔ window run 15 (120.981453ms)
✔ window run 16 (115.099923ms)
✔ window run 17 (118.736257ms)
✔ window run 18 (134.113383ms)
✔ window run 19 (126.189466ms)
✔ window run 20 (118.381386ms)
✔ window run 21 (123.819604ms)
✔ window run 22 (124.274617ms)
✔ window run 23 (119.065609ms)
✔ window run 24 (115.889118ms)
✔ window run 25 (113.572234ms)
✔ window run 26 (122.461122ms)
✔ window run 27 (116.525172ms)
✔ window run 28 (112.16788ms)
✔ window run 29 (124.721334ms)
✔ window run 30 (117.365707ms)
✔ window run 31 (116.529775ms)
✔ window run 32 (118.083284ms)
✔ window run 33 (118.283334ms)
✔ window run 34 (120.27732ms)
✔ window run 35 (120.791974ms)
✔ window run 36 (124.325033ms)
✔ window run 37 (118.66312ms)
✔ window run 38 (122.882066ms)
TALLY {"SIG":"SIGKILL","SPIN":1.5,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":8,"errorEvents":{}}
✔ window run 39 (117.968416ms)
✔ tally (0.348867ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 4927.807788
@@ -0,0 +1,50 @@
✔ window run 0 (144.224291ms)
✔ window run 1 (129.396254ms)
✔ window run 2 (128.07545ms)
✔ window run 3 (128.077895ms)
✔ window run 4 (118.253611ms)
✔ window run 5 (120.717634ms)
✔ window run 6 (123.163777ms)
✔ window run 7 (118.897388ms)
✔ window run 8 (119.303061ms)
✔ window run 9 (120.444716ms)
✔ window run 10 (124.18566ms)
✔ window run 11 (118.053805ms)
✔ window run 12 (115.225239ms)
✔ window run 13 (120.643862ms)
✔ window run 14 (122.46287ms)
✔ window run 15 (117.717123ms)
✔ window run 16 (123.773426ms)
✔ window run 17 (120.774322ms)
✔ window run 18 (138.138442ms)
✔ window run 19 (133.690503ms)
✔ window run 20 (125.271627ms)
✔ window run 21 (131.574194ms)
✔ window run 22 (119.464835ms)
✔ window run 23 (125.506749ms)
✔ window run 24 (155.395426ms)
✔ window run 25 (178.540602ms)
✔ window run 26 (126.017958ms)
✔ window run 27 (121.052863ms)
✔ window run 28 (124.295782ms)
✔ window run 29 (126.573336ms)
✔ window run 30 (122.492436ms)
✔ window run 31 (121.830347ms)
✔ window run 32 (117.744637ms)
✔ window run 33 (118.159241ms)
✔ window run 34 (116.895203ms)
✔ window run 35 (121.259299ms)
✔ window run 36 (123.21738ms)
✔ window run 37 (130.53615ms)
✔ window run 38 (131.87434ms)
TALLY {"SIG":"SIGKILL","SPIN":1.8,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":7,"errorEvents":{}}
✔ window run 39 (114.693736ms)
✔ tally (0.315463ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5110.552176
@@ -0,0 +1,50 @@
✔ window run 0 (127.58092ms)
✔ window run 1 (162.627319ms)
✔ window run 2 (127.693204ms)
✔ window run 3 (125.629054ms)
✔ window run 4 (119.967212ms)
✔ window run 5 (118.150815ms)
✔ window run 6 (180.734978ms)
✔ window run 7 (140.129292ms)
✔ window run 8 (130.352157ms)
✔ window run 9 (119.357903ms)
✔ window run 10 (144.928105ms)
✔ window run 11 (401.668465ms)
✔ window run 12 (461.248243ms)
✔ window run 13 (650.094085ms)
✔ window run 14 (434.334983ms)
✔ window run 15 (172.035638ms)
✔ window run 16 (128.637775ms)
✔ window run 17 (128.864828ms)
✔ window run 18 (122.010379ms)
✔ window run 19 (123.539318ms)
✔ window run 20 (123.093834ms)
✔ window run 21 (130.221603ms)
✔ window run 22 (122.416697ms)
✔ window run 23 (150.960852ms)
✔ window run 24 (128.917022ms)
✔ window run 25 (134.380817ms)
✔ window run 26 (125.219328ms)
✔ window run 27 (132.753926ms)
✔ window run 28 (122.113979ms)
✔ window run 29 (122.278419ms)
✔ window run 30 (122.297869ms)
✔ window run 31 (120.82054ms)
✔ window run 32 (132.674865ms)
✔ window run 33 (128.975462ms)
✔ window run 34 (125.417454ms)
✔ window run 35 (120.8199ms)
✔ window run 36 (130.788311ms)
✔ window run 37 (128.351075ms)
✔ window run 38 (149.992294ms)
TALLY {"SIG":"SIGKILL","SPIN":2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":0,"errorEvents":{}}
✔ window run 39 (120.503523ms)
✔ tally (0.354952ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6774.656592
@@ -0,0 +1,50 @@
✔ window run 0 (136.774204ms)
✔ window run 1 (127.897477ms)
✔ window run 2 (120.689443ms)
✔ window run 3 (151.123263ms)
✔ window run 4 (155.605552ms)
✔ window run 5 (120.71843ms)
✔ window run 6 (124.778061ms)
✔ window run 7 (135.363919ms)
✔ window run 8 (126.663916ms)
✔ window run 9 (120.581723ms)
✔ window run 10 (123.88765ms)
✔ window run 11 (124.882036ms)
✔ window run 12 (150.339719ms)
✔ window run 13 (123.303618ms)
✔ window run 14 (127.070752ms)
✔ window run 15 (128.472236ms)
✔ window run 16 (118.586597ms)
✔ window run 17 (125.180194ms)
✔ window run 18 (120.451018ms)
✔ window run 19 (128.182541ms)
✔ window run 20 (123.20001ms)
✔ window run 21 (122.100919ms)
✔ window run 22 (119.403525ms)
✔ window run 23 (173.776168ms)
✔ window run 24 (134.439389ms)
✔ window run 25 (130.349125ms)
✔ window run 26 (127.978361ms)
✔ window run 27 (136.333253ms)
✔ window run 28 (120.925399ms)
✔ window run 29 (129.149668ms)
✔ window run 30 (132.693504ms)
✔ window run 31 (123.656819ms)
✔ window run 32 (126.766868ms)
✔ window run 33 (120.573641ms)
✔ window run 34 (135.714167ms)
✔ window run 35 (135.710477ms)
✔ window run 36 (117.249605ms)
✔ window run 37 (119.018592ms)
✔ window run 38 (131.310506ms)
TALLY {"SIG":"SIGKILL","SPIN":2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":0,"errorEvents":{}}
✔ window run 39 (120.801524ms)
✔ tally (0.334269ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5251.675554
@@ -0,0 +1,40 @@
✔ window run 0 (132.487146ms)
✔ window run 1 (124.968965ms)
✔ window run 2 (124.213142ms)
✔ window run 3 (120.3321ms)
✔ window run 4 (124.944505ms)
✔ window run 5 (142.543814ms)
✔ window run 6 (127.955434ms)
✔ window run 7 (125.514636ms)
✔ window run 8 (136.942164ms)
✔ window run 9 (138.072103ms)
✔ window run 10 (129.780512ms)
✔ window run 11 (118.296274ms)
✔ window run 12 (122.711134ms)
✔ window run 13 (121.931431ms)
✔ window run 14 (110.439928ms)
✔ window run 15 (117.012974ms)
✔ window run 16 (117.55668ms)
✔ window run 17 (128.429878ms)
✔ window run 18 (122.460641ms)
✔ window run 19 (138.367156ms)
✔ window run 20 (117.440095ms)
✔ window run 21 (126.05533ms)
✔ window run 22 (129.696191ms)
✔ window run 23 (117.591068ms)
✔ window run 24 (118.745547ms)
✔ window run 25 (129.883241ms)
✔ window run 26 (118.206089ms)
✔ window run 27 (118.678953ms)
✔ window run 28 (129.870729ms)
TALLY {"SIG":"SIGKILL","SPIN":0,"runs":30,"rejected":{"1: notifier exited (null) before it replied":30},"closeSends":30,"errorEvents":{}}
✔ window run 29 (121.367229ms)
✔ tally (0.391525ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3825.283784
@@ -0,0 +1,40 @@
✔ window run 0 (123.23705ms)
✔ window run 1 (123.961923ms)
✔ window run 2 (125.332242ms)
✔ window run 3 (119.429363ms)
✔ window run 4 (115.543357ms)
✔ window run 5 (112.826753ms)
✔ window run 6 (116.923424ms)
✔ window run 7 (116.256691ms)
✔ window run 8 (113.931748ms)
✔ window run 9 (123.418455ms)
✔ window run 10 (121.795499ms)
✔ window run 11 (119.200194ms)
✔ window run 12 (120.635779ms)
✔ window run 13 (120.632373ms)
✔ window run 14 (124.328492ms)
✔ window run 15 (121.649256ms)
✔ window run 16 (117.564938ms)
✔ window run 17 (151.390084ms)
✔ window run 18 (117.289768ms)
✔ window run 19 (129.194512ms)
✔ window run 20 (123.363796ms)
✔ window run 21 (142.082497ms)
✔ window run 22 (121.358209ms)
✔ window run 23 (355.482528ms)
✔ window run 24 (119.360699ms)
✔ window run 25 (116.390105ms)
✔ window run 26 (118.809952ms)
✔ window run 27 (126.164074ms)
✔ window run 28 (119.761348ms)
TALLY {"SIG":"SIGKILL","SPIN":1,"runs":30,"rejected":{"1: notifier exited (null) before it replied":30},"closeSends":25,"errorEvents":{}}
✔ window run 29 (115.468358ms)
✔ tally (0.338162ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3968.902661
@@ -0,0 +1,40 @@
✔ window run 0 (122.416389ms)
✔ window run 1 (123.205492ms)
✔ window run 2 (121.140654ms)
✔ window run 3 (134.247788ms)
✔ window run 4 (136.490509ms)
✔ window run 5 (120.542247ms)
✔ window run 6 (129.277154ms)
✔ window run 7 (125.004173ms)
✔ window run 8 (120.097454ms)
✔ window run 9 (118.368948ms)
✔ window run 10 (116.042869ms)
✔ window run 11 (127.958125ms)
✔ window run 12 (121.512347ms)
✔ window run 13 (116.729365ms)
✔ window run 14 (124.872986ms)
✔ window run 15 (119.443772ms)
✔ window run 16 (117.00815ms)
✔ window run 17 (116.119014ms)
✔ window run 18 (119.493366ms)
✔ window run 19 (125.80199ms)
✔ window run 20 (126.38055ms)
✔ window run 21 (117.431147ms)
✔ window run 22 (120.440654ms)
✔ window run 23 (127.229032ms)
✔ window run 24 (124.86833ms)
✔ window run 25 (121.312387ms)
✔ window run 26 (121.249029ms)
✔ window run 27 (126.269621ms)
✔ window run 28 (128.307128ms)
TALLY {"SIG":"SIGKILL","SPIN":2,"runs":30,"rejected":{"1: notifier exited (null) before it replied":30},"closeSends":0,"errorEvents":{}}
✔ window run 29 (118.851188ms)
✔ tally (0.379132ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3758.865814
@@ -0,0 +1,40 @@
✔ window run 0 (136.105562ms)
✔ window run 1 (126.816338ms)
✔ window run 2 (119.846457ms)
✔ window run 3 (121.782267ms)
✔ window run 4 (128.762498ms)
✔ window run 5 (119.906631ms)
✔ window run 6 (123.896163ms)
✔ window run 7 (119.086444ms)
✔ window run 8 (120.050441ms)
✔ window run 9 (118.135927ms)
✔ window run 10 (118.249898ms)
✔ window run 11 (129.107726ms)
✔ window run 12 (126.131622ms)
✔ window run 13 (124.325597ms)
✔ window run 14 (122.411699ms)
✔ window run 15 (121.811091ms)
✔ window run 16 (122.758996ms)
✔ window run 17 (123.200781ms)
✔ window run 18 (135.185419ms)
✔ window run 19 (147.180265ms)
✔ window run 20 (122.065511ms)
✔ window run 21 (126.140412ms)
✔ window run 22 (124.599734ms)
✔ window run 23 (122.169503ms)
✔ window run 24 (120.563634ms)
✔ window run 25 (123.590214ms)
✔ window run 26 (125.482938ms)
✔ window run 27 (128.60004ms)
✔ window run 28 (138.365322ms)
TALLY {"SIG":"SIGKILL","SPIN":5,"runs":30,"rejected":{"1: notifier exited (null) before it replied":30},"closeSends":0,"errorEvents":{}}
✔ window run 29 (120.901641ms)
✔ tally (0.425858ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3836.882858
@@ -0,0 +1,14 @@
Fri Oct 9 02:13:28 PM UTC 2026
cand SPIN 1.2 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":16,"errorEvents":{}} unhandled=0
nocb SPIN 1.2 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":34,"undefined: write EPIPE":6},"closeSends":20,"errorEvents":{}} unhandled=0
cand SPIN 1.5 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.5,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":17,"errorEvents":{}} unhandled=0
nocb SPIN 1.5 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.5,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":8,"errorEvents":{}} unhandled=0
cand SPIN 1.8 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.8,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":3,"errorEvents":{}} unhandled=0
nocb SPIN 1.8 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.8,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":7,"errorEvents":{}} unhandled=0
Fri Oct 9 02:14:18 PM UTC 2026
cand SPIN 1.0 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":34,"errorEvents":{}} unhandled=0
cand SPIN 1.2 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":23,"errorEvents":{}} unhandled=0
cand SPIN 1.2 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":18,"errorEvents":{}} unhandled=0
cand SPIN 1.2 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":32,"errorEvents":{}} unhandled=0
cand SPIN 1.4 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1.4,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":24,"errorEvents":{}} unhandled=0
Fri Oct 9 02:14:52 PM UTC 2026
@@ -0,0 +1,8 @@
09:12:51 up 33 days, 10:47, 5 users, load average: 3.31, 2.79, 2.53
nolisten batch 1 rc=0 TALLY {"SIG":"SIGKILL","SPIN":2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":0,"errorEvents":{}} ℹ fail 0 unhandled=0
nolisten batch 2 rc=0 TALLY {"SIG":"SIGKILL","SPIN":2,"runs":40,"rejected":{"1: notifier exited (null) before it replied":40},"closeSends":0,"errorEvents":{}} ℹ fail 0 unhandled=0
listener SPIN 0 rc=0 TALLY {"SIG":"SIGKILL","SPIN":0,"runs":30,"rejected":{"1: notifier exited (null) before it replied":30},"closeSends":30,"errorEvents":{}} ℹ fail 0
listener SPIN 1 rc=0 TALLY {"SIG":"SIGKILL","SPIN":1,"runs":30,"rejected":{"1: notifier exited (null) before it replied":30},"closeSends":25,"errorEvents":{}} ℹ fail 0
listener SPIN 2 rc=0 TALLY {"SIG":"SIGKILL","SPIN":2,"runs":30,"rejected":{"1: notifier exited (null) before it replied":30},"closeSends":0,"errorEvents":{}} ℹ fail 0
listener SPIN 5 rc=0 TALLY {"SIG":"SIGKILL","SPIN":5,"runs":30,"rejected":{"1: notifier exited (null) before it replied":30},"closeSends":0,"errorEvents":{}} ℹ fail 0
09:13:19 up 33 days, 10:48, 5 users, load average: 3.33, 2.85, 2.55
@@ -0,0 +1,19 @@
new dir opens
missing dir, parent 0500 CliError exit=3 code=-: notify journal directory cannot be created (EACCES): <tmp>/c1/acme
dir 0500 CliError exit=3 code=-: notify journal directory is not writable (EACCES): <tmp>/c2
dir 0300 (no read) opens
dir 0755 CliError exit=3 code=-: notify journal directory must be mode 0700 and owned by this user: <tmp>/c4
dir symlink to 0700 CliError exit=3 code=-: notify journal directory must not be a symlink: <tmp>/c6
dir dangling symlink CliError exit=3 code=-: notify journal directory must not be a symlink: <tmp>/c7
dir path is a file CliError exit=3 code=-: notify journal directory must be mode 0700 and owned by this user: <tmp>/c8
parent is a file CliError exit=3 code=-: notify journal directory cannot be created (ENOTDIR): <tmp>/c9/acme
file 0400 CliError exit=3 code=-: notify journal is not writable (EACCES): <tmp>/c10/sent.jsonl
file 0644 CliError exit=3 code=-: notify journal must be a regular file, mode 0600, owned by this user: <tmp>/c11/sent.jsonl
file path is a dir CliError exit=3 code=-: notify journal must be a regular file, mode 0600, owned by this user: <tmp>/c12/sent.jsonl
gave-up dm opens
gave-up digest CliError exit=3 code=-: notify journal line 1 is malformed (outcome): <tmp>/c14/sent.jsonl
refused dm status 403 opens
refused dm status "403" CliError exit=3 code=-: notify journal line 1 is malformed (status): <tmp>/c16/sent.jsonl
digest day 2026-13-45 CliError exit=3 code=-: notify journal line 1 is malformed (day): <tmp>/c17/sent.jsonl
at without ms CliError exit=3 code=-: notify journal line 1 is malformed (at): <tmp>/c18/sent.jsonl
dm decision 7 CliError exit=3 code=-: notify journal line 1 is malformed (decision): <tmp>/c19/sent.jsonl
@@ -0,0 +1,16 @@
Fri Oct 9 02:14:58 PM UTC 2026
20 rc=0 {"delay":"sync","cb":false,"connected":true,"sent":true,"errorEvent":null}
20 rc=0 {"delay":"immediate","cb":false,"connected":true,"sent":true,"errorEvent":null}
6 rc=0 {"delay":"1","cb":false,"connected":false,"sent":null,"errorEvent":null}
11 rc=0 {"delay":"1","cb":false,"connected":true,"sent":true,"errorEvent":null}
3 rc=1 node:events:505 throw er; // Unhandled 'error' event ^ Error: write EPIPE
20 rc=0 {"delay":"2","cb":false,"connected":false,"sent":null,"errorEvent":null}
20 rc=0 {"delay":"5","cb":false,"connected":false,"sent":null,"errorEvent":null}
20 rc=0 {"delay":"sync","cb":true,"connected":true,"sent":true,"cbErr":null,"errorEvent":null}
20 rc=0 {"delay":"immediate","cb":true,"connected":true,"sent":true,"cbErr":null,"errorEvent":null}
8 rc=0 {"delay":"1","cb":true,"connected":false,"sent":null,"errorEvent":null}
10 rc=0 {"delay":"1","cb":true,"connected":true,"sent":true,"cbErr":"EPIPE","errorEvent":null}
2 rc=0 {"delay":"1","cb":true,"connected":true,"sent":true,"cbErr":null,"errorEvent":null}
20 rc=0 {"delay":"2","cb":true,"connected":false,"sent":null,"errorEvent":null}
20 rc=0 {"delay":"5","cb":true,"connected":false,"sent":null,"errorEvent":null}
Fri Oct 9 02:15:44 PM UTC 2026
@@ -0,0 +1,53 @@
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (901.128701ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (200.974735ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (143.839337ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (177.949725ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (166.371407ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (109.334297ms)
✖ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (169.53424ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (113.379595ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (180.612226ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (25.853366ms)
✔ bus stop refuses to signal a live pid that is not a bus host (203.250525ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (229.322779ms)
✔ bus start refuses with exit 3 without a notifier config (100.568013ms)
✔ bus start runs until bus stop; status reports it while it runs (695.909159ms)
✔ bus-service.sh renders the unit and installs it into a given directory (38.912292ms)
ℹ tests 15
ℹ suites 0
ℹ pass 14
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 22035.449879
✖ failing tests:
test at packages/cli/tests/host.test.mjs:271:1
✖ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (169.53424ms)
AssertionError [ERR_ASSERTION]: The validation function is expected to return "true". Received false
Caught error:
Error: write EPIPE
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/tests/host.test.mjs:279:3)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: Error: write EPIPE
at epipe (file:///home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/tests/host.test.mjs:113:24)
at file:///home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/tests/host.test.mjs:276:51
at ChildProcess.send (file:///home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/tests/host.test.mjs:94:23)
at startHost (file:///home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/src/host.mjs:150:36)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async waitForActual (node:assert:615:5)
at async strict.rejects (node:assert:738:25)
at async TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/tests/host.test.mjs:279:3)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7),
operator: 'rejects',
diff: 'simple'
}
@@ -0,0 +1,57 @@
✔ zoned uses the IANA zone across DST (13.576673ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (59.501778ms)
✔ two blocking decisions get two DMs with different nonces (53.051795ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.204649ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (47.675935ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (46.418663ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (53.547704ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (46.317859ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (108.858391ms)
✖ a restart after the second refusal does not send before that refusal's 30 min are up (79.688197ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (55.259672ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (42.118885ms)
✔ an inbox read failure is logged and the next poll retries (0.64199ms)
✔ no Discord id reaches the journal or the log (46.664543ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (10.28155ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (14.793831ms)
✔ the journal: a whole file that is one torn line truncates to empty (9.68584ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.597578ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.571636ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.872573ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.302665ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.455729ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.364107ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.452604ms)
✔ digest content stays within Discord's 2000 characters (0.270363ms)
✔ runLoop never overlaps ticks and stops after the one in flight (110.198965ms)
ℹ tests 26
ℹ suites 0
ℹ pass 25
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 867.94662
✖ failing tests:
test at packages/cli/tests/notifier.test.mjs:201:1
✖ a restart after the second refusal does not send before that refusal's 30 min are up (79.688197ms)
AssertionError [ERR_ASSERTION]: nothing between the restart and +60 min
+ actual - expected
[
+ 31.5
- 60
]
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/tests/notifier.test.mjs:208:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: [ 31.5 ],
expected: [ 60 ],
operator: 'deepStrictEqual',
diff: 'simple'
}
@@ -0,0 +1,55 @@
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (904.392647ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (195.161563ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (112.904093ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (183.76143ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (167.268086ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (105.710329ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (170.287733ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (126.009082ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (171.530803ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (23.647599ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.494767ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (234.236466ms)
✔ bus start refuses with exit 3 without a notifier config (97.13829ms)
✔ bus start runs until bus stop; status reports it while it runs (674.314841ms)
✖ bus-service.sh renders the unit and installs it into a given directory (19.236597ms)
ℹ tests 15
ℹ suites 0
ℹ pass 14
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3481.947593
✖ failing tests:
test at packages/cli/tests/host.test.mjs:396:1
✖ bus-service.sh renders the unit and installs it into a given directory (19.236597ms)
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /^ {2}mkdir -m 0700 -p <dataRoot>\/notify\/<business> +the notifier refuses a looser directory$/m. Input:
'written: /home/jwoltje/darkwing-scratch/tmp/mosaic-cli-BJ6nOZ/[email protected]\n' +
'next, for one business (one per data root):\n' +
' write <dataRoot>/notify/<business>/notify.json, mode 0600:\n' +
' {"notifyVersion": 1, "binding": "<discord binding>"} or "binding": null for no DMs\n' +
' systemctl --user enable --now mosaic-bus@<business> start now and at login\n' +
' systemctl --user status mosaic-bus@<business>\n' +
" journalctl --user -u mosaic-bus@<business> -f the host's log\n" +
' systemctl --user stop mosaic-bus@<business> SIGTERM; restartable\n' +
'survive logout and reboot only with lingering on: loginctl enable-linger jwoltje\n'
at TestContext.<anonymous> (file:///home/jwoltje/darkwing-scratch/r45b/wt/packages/cli/tests/host.test.mjs:409:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: `written: /home/jwoltje/darkwing-scratch/tmp/mosaic-cli-BJ6nOZ/[email protected]\nnext, for one business (one per data root):\n write <dataRoot>/notify/<business>/notify.json, mode 0600:\n {"notifyVersion": 1, "binding": "<discord binding>"} or "binding": null for no DMs\n systemctl --user enable --now mosaic-bus@<business> start now and at login\n systemctl --user status mosaic-bus@<business>\n journalctl --user -u mosaic-bus@<business> -f the host's log\n systemctl --user stop mosaic-bus@<business> SIGTERM; restartable\nsurvive logout and reboot only with lingering on: loginctl enable-linger jwoltje\n`,
expected: /^ {2}mkdir -m 0700 -p <dataRoot>\/notify\/<business> +the notifier refuses a looser directory$/m,
operator: 'match',
diff: 'simple'
}
@@ -0,0 +1,32 @@
// Row 45 round 2 probe: decision 73's wait across restarts. A fake clock
// polls every POLL_MS against a DM transport that always answers 403; at
// each restart minute a fresh notifier opens the same journal. Prints the
// DM send minutes and the gave-up minute.
// node restart-wait.mjs <packages dir> <restart minutes, comma list>
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { join } from "node:path";
const W = process.argv[2];
const restarts = (process.argv[3] ?? "").split(",").filter(Boolean).map(Number);
const { createNotifier, journalPath, POLL_MS } = await import(`${W}/cli/src/notifier.mjs`);
const { RestOutcome } = await import(`${W}/discord/src/rest.mjs`);
const root = mkdtempSync(join(process.env.TMPDIR, "dw-r45-rw-"));
const t0 = Date.parse("2026-10-09T03:00:00.000Z"); // digest sends are refused too but not counted
let t = t0;
const sends = [];
const make = () => createNotifier({
business: "acme", dataRoot: root,
inbox: async () => [{ id: "dec-0001-aaaa", blocking: true, action: "approve", question: "q", options: [{ key: "yes", text: "yes" }], created: "2026-10-09T02:00:00.000Z", requester: "r" }],
direct: { send: async (m) => { if (!String(m?.content).startsWith("Mosaic digest")) sends.push(t); throw new RestOutcome("refused", "dm: refused", { status: 403 }); } },
now: () => new Date(t), log: () => {},
});
let n = make();
const min = (x) => (x - t0) / 60000;
for (let i = 0; i < 400; i++) {
if (restarts.includes(min(t))) n = make();
await n.tick();
t += POLL_MS;
}
const lines = readFileSync(journalPath(root, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l));
const gave = lines.filter((l) => l.outcome === "gave-up").map((l) => min(Date.parse(l.at)));
console.log(`restarts at ${restarts.join(", ") || "none"}; dm sends at ${sends.map(min).join(", ")}; gave-up lines at ${gave.join(", ")}`);
rmSync(root, { recursive: true, force: true });
@@ -0,0 +1,6 @@
restarts at none; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 31; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 0.5, 29.5, 59.5; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 89.5, 91, 119.5; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 120.5; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 1, 2, 3, 4, 5, 31, 61, 91, 121; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
@@ -0,0 +1,176 @@
# Row 45, S4 follow-up, round 2 review (Darkwing)
Issue #1527, request comment 26882, queue rev 216 (`19dcc553`).
Packet: `agents/rocko/work/s4-follow-up/` at `af1377d7`, base `d539d8d2`,
8 files, +635/-56. Candidate manifest sha256
`5b067a9dad645c31d99e1ea02575cd2fc1ba547ce011ea81c56b17ae29da0d0e`,
`build.patch` sha256
`c8cec070da60d8297f1ee5b006a1099ab9376fd3abf32fa4461f967750ad6756`.
Rulings: lead decisions 72 and 73. My round 1: `review-r1.md`.
Verdict: **approve**, comment 26884. R1 and R2 are fixed and hold
under my probes. Notes 1 to 4 are taken and checked. Rocko's decline of
the type check in `append` is sound.
## Method
- A detached worktree at `d539d8d2`, then `git apply --index build.patch`
and `sha256sum -c candidate-manifest.sha256`: 8 OK. Both digests match
`git show af1377d7:<path> | sha256sum`. After the mutants I restored the
files and checked the manifest again: 8 OK.
- I read the diff for `host.mjs`, `notifier.mjs`, `bus-service.sh`, the
cli README and the new tests, against decisions 72 and 73, and Rocko's
round 2 section in `BUILD.md`.
- Probes in `probe-r2/`. `giveup-time.mjs`, `journal-paths.mjs` and
`late-send.mjs` are the round 1 files in `probe/`, unchanged.
`host-window.test.mjs` differs only in its import paths. New:
`restart-wait.mjs`. They import from my scratch worktree by absolute
path, so they won't run as committed without editing the paths.
- Three mutants of my own: Mb drops the callback at `host.mjs:150`, Mc
ignores `refusedAt` in `tick`, Md2 drops the `mkdir` line from the
install message.
Node is v26.8.1, `TMPDIR=~/darkwing-scratch/tmp`, load about 3.3.
## Suites
| Suite | Result | File |
|---|---|---|
| `node --test 'packages/cli/tests/*.test.mjs'` | 66/66 | `out-r2/node-cli.txt` |
| `node --test 'packages/discord/tests/*.test.mjs'` | 178/178 | `out-r2/node-discord.txt` |
| `node --test 'packages/bus/tests/*.test.mjs'` | 67/67 | `out-r2/node-bus.txt` |
| `node --test 'packages/tasks/tests/*.test.mjs'` | 51/51 | `out-r2/node-tasks.txt` |
| `scripts/test-discord.sh` | 66 passed, 0 failed | `out-r2/test-discord.txt` |
Sage's gate covers the rest.
## R1: the close and stop sends (closed)
`host.mjs:144`, `:150` and `:188` now read
`if (broker.connected) broker.send({ op: "close" }, () => {});`, and
`:184` sends `stop` to the notifier the same way. The three new host tests
use the deterministic form from round 1: `failSends` passes `EPIPE` to the
callback if there is one, and otherwise emits `'error'` on the next tick.
Through `startHost` (`probe-r2/host-window.test.mjs`), the SPIN 2 window
from round 1 didn't open this time. All 80 runs at SPIN 2 with no listener
made 0 close sends. Timing drifts with load, so I swept SPIN from 1.0 to
1.8 and ran a no-callback control beside the candidate
(`probe-r2/hw-r2-sweep.txt`):
| SPIN | Candidate, 40 runs | No-callback control, 40 runs |
|---|---|---|
| 1.2 | 16 close sends, 0 `EPIPE` | 20 close sends, 6 reject with a raw `write EPIPE` |
| 1.5 | 17 close sends, 0 `EPIPE` | 8 close sends, 0 `EPIPE` |
| 1.8 | 3 close sends, 0 `EPIPE` | 7 close sends, 0 `EPIPE` |
Five more candidate batches at SPIN 1.0, 1.2, 1.2, 1.2 and 1.4 made 34,
23, 18, 32 and 24 close sends. Across all 520 candidate runs (`hw-r2.txt`
and the sweep), 222 close sends went out. Every run rejected with the
notifier's error, none with `write EPIPE`, and none left an unhandled
rejection. The control shows the window was reachable at the same time,
so the zero is the fix and not a quiet machine.
The bare fork (`probe-r2/late-send-r2.txt`) still shows the Node
behaviour the fix relies on. At 1 ms after SIGKILL, 3 of 20 runs with no
callback crash on an unhandled `'error'` (`write EPIPE`). With a callback,
10 of 20 pass `EPIPE` to it, with 0 `'error'` events and 0 crashes. This
probe tests Node, not the candidate.
Mutant Mb (no callback at `:150`) fails the host test "a close send that
fails with EPIPE after the notifier refuses still gives the notifier's
refusal, exit 3": pass 14, fail 1 (`probe-r2/mut-Mb-nocb150.txt`).
Rocko's G144cb, G150cb, G184 and G188 cover the other sites.
## R2: decision 73's wait (closed)
A definite refusal now waits `BACKOFF_MAX_MS`, 30 min. `count()` keeps
`refusedAt`, the `at` of the last definite refusal, and `tick` skips a
decision until `refusedAt` plus 30 min. The wait therefore comes from the
journal, not from memory.
`probe-r2/giveup-time-r2.txt`, every send refused 403: the DM goes out at
0, 30, 60, 90 and 120 min, each with "retry in 1800 s", then
"dm dec-0001 refused 5 times; gave up, not retried" at 120 min. That
matches decision 73's 2 h span.
`probe-r2/restart-wait.mjs` starts a fresh notifier on the same journal
at given minutes and counts only DM sends (`restart-wait.txt`):
```
restarts at none; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 31; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 0.5, 29.5, 59.5; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 89.5, 91, 119.5; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 120.5; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
restarts at 1, 2, 3, 4, 5, 31, 61, 91, 121; dm sends at 0, 30, 60, 90, 120; gave-up lines at 120
```
No restart schedule shortens a wait or adds a send, and a restart after
the give-up writes no second `gave-up` line.
Mutant Mc (`tick` ignores `refusedAt`) fails "a restart after the second
refusal does not send before that refusal's 30 min are up": pass 25,
fail 1 (`probe-r2/mut-Mc-norefusedat.txt`).
The README now says recovery after a give-up is manual: the decision
stays in `mosaic inbox` and the digest, and the operator decides it with
`mosaic decide`. Round 1 asked for that line under option 2.
## Round 1 notes
1. **Raw errors (taken).** Each path in `probe-r2/journal-paths-r2.txt`
is now a `CliError` with exit 3. A dangling symlink gives the symlink
message, a parent that is a file gives "cannot be created (ENOTDIR)",
and a `sent.jsonl` that is a directory gives the regular-file message.
The round 1 paths are unchanged.
2. **The `mkdir` line (taken).** Mutant Md2 drops it and fails "bus-service.sh
renders the unit and installs it into a given directory": pass 14,
fail 1 (`probe-r2/mut-Md2-nomkdir.txt`). In round 1 the same mutant
passed everything.
3. **The day check (taken).** `isDay` checks the shape, then
`Date.parse`, then a `toISOString` round trip. `2026-13-45` refuses as
`malformed (day)`.
4. **The README sentence (taken).** It now names three states in quotes.
5. **`accessSync` (unchanged).** It was a note with no change asked.
## Rocko's decline
Filbert asked for a type check in `append`. Rocko declined, and I agree.
In `attempt` (`notifier.mjs:271`), the `confirmed` append sits in the
`try` after `direct.send` returns. A throw from it lands in the `catch`,
which journals `unknown` for a DM that Discord already delivered, and
every later tick would send it again. `rest.mjs` already throws `unknown`
on a 2xx without a string id, so the writer can't produce that line
today. If it ever does, the open-time check refuses it with exit 3, which
is the loud failure.
## Notes (not blocking)
1. A digest refused with a definite 403 retries without a limit, with
doubling up to 30 min (`giveup-time-r2.txt`, 13:00Z on). That follows
decision 72, which limits DMs only. A digest refused for a bad token
then retries every 30 min forever, and each retry logs a line. One
sentence in the README or the header comment would say this is meant.
2. `refusedAt` comes from the journal's `at`. If the clock steps back
after a refusal, the wait grows by the step. The fail direction is
"wait longer", which is safe. I note it only.
3. A journal directory path that is a regular file gives "must be mode
0700 and owned by this user". The refusal is right; the message could
say "not a directory". Cosmetic.
## Files
- `review-r2.md`, this file.
- `out-r2/`: the suite outputs and `summary.txt`.
- `probe-r2/host-window.test.mjs` and `hw-r2*.txt`: the `startHost`
window, the sweep and its no-callback control.
- `probe-r2/late-send-r2.txt`: the bare fork, run with `probe/late-send.mjs`.
- `probe-r2/giveup-time-r2.txt`: decision 73 timing, run with
`probe/giveup-time.mjs`.
- `probe-r2/restart-wait.mjs`, `probe-r2/restart-wait.txt`: restarts
against the wait.
- `probe-r2/journal-paths-r2.txt`: journal error paths and types, run
with `probe/journal-paths.mjs`.
- `probe-r2/mut-Mb-nocb150.txt`, `mut-Mc-norefusedat.txt`,
`mut-Md2-nomkdir.txt`: the mutants.