An instrument for a behavioural test
Voyager's success test is that Dad never reopens another browser. A test like that needs an instrument that notices when he does, and records it without anyone remembering to. Invariant 10 states it in three words, departures are data: when another browser comes to the front, Voyager records it; fixes are ordered by that log; a departure with no recorded reason is still a departure.
The implementation is small enough to read in one sitting, and it is a clean example of a pattern worth knowing: an append-only event file, folded into state when it is read.
Four events, one file that only grows
The log lives in departures.jsonl in Voyager's folder, one JSON object per line. There are four kinds of event:
| Event | Wire name | When |
|---|---|---|
departed | departed | Another browser came to the front: an id, the time, the browser's name and bundle id, and the host Dad had in front in Voyager, if any |
seenAgain | seen_again | The same browser came forward again before Dad returned to Voyager |
asked | asked | The question about that departure was shown |
reason | reason | Dad typed why |
Writing is append(_:): encode the event with sorted keys and ISO 8601 dates, add a newline, seek to the end, write. Nothing is ever rewritten. A reason arriving a minute later does not edit the departed line; it becomes a line of its own.
Reading is a fold. read() decodes every line it can, and fold(_:) replays the events in file order into one Departure row each: departed creates the row, seen_again moves its last-seen time and counts one more activation, asked marks it, reason fills in Dad's words. One stay in another browser is one row, however many times he switched to it during that stay.
The line this build cannot read
The detail I like most is what happens to a line that does not decode. It is counted in unreadableLines and left exactly where it is. The test for it appends a line of a kind no build knows, from_the_future, and checks two things: the count is 1, and the file still has all six lines afterward.
That one rule buys forward compatibility for free. A newer build can add a fifth kind of event, and an older build reading the same file loses nothing: it folds what it understands and reports what it skipped. A store that "repaired" unreadable lines would destroy exactly the data it did not understand.
Who counts as another browser
The watcher in the Mac shell listens for NSWorkspace.didActivateApplicationNotification. Every time an app comes to the front it asks one question: is this a browser? The first answer was any app the Mac offers for web addresses, and the log promptly filled with noise. Apps that catch an https link only to finish their own sign-in were counted as browsers. The source comment records the number: the Codex app was 274 of the first 623 departures.
The rule since 2026-10-22 is an intersection: an app counts when this Mac offers it both for a web address and for a web page on disk (UTType.html), and never Voyager itself or its verification build. The watcher asks Launch Services for both sets, with https://example.com/ as the probe address, and refreshes the answer at most once a minute. There is no list of browsers anywhere in the source. A browser installed tomorrow counts tomorrow.
The watcher also decides what a departure says about the page Dad left. It records leftFrom as the host of the front tab, never the full address. That is enough to tell which site sent him elsewhere and no more.
A second reader proves the format
The file format is a contract, and Voyager is not its only reader. scripts/doctor.sh, a read-only report of what is built, what is installed and what state Voyager holds, reads the same file with a few lines of Python and prints how many departures are recorded and how many still have no reason. It does not import any Swift. If the doctor can count the log, the log is data that outlives any one program reading it.
What the log orders
The architecture splits Dad's browsing into two halves. The family half (his apps and Pippa) is where existing browsers are worst, so Voyager works that half first, by design. The outside half (everything else on the web) is worked in the order the log gives. Even the engine decision defers to it: the reserved Chromium option comes back only if departures show Dad needs it.
The contract adds one fair exception. Opening another browser to study that browser, for example to try another browser's assistant or reproduce a Chrome-only bug in a family app, is recorded like any other departure and closed with that reason. It orders no work.
One scope note: the log is the Mac's. The iPhone and iPad shell keeps no departure log, and nothing under clients/apple/VoyagerMobile writes one. The parity contract's table of what the phone app does not do yet leaves this difference out, so it is a fact of the code, not a named gap.