Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
f50f041
Backstop: end a valid session when the app is gone, the battery is be…
krishhgg Oct 1, 2026
5e2440c
Backstop: adopt the agent's end in the app, bound the reads, type-che…
krishhgg Oct 2, 2026
ebc72bd
Merge branch 'main' into fix/out-of-process-cutoff
krishhgg Oct 2, 2026
a357b91
Config: move a default the 24-hour ceiling no longer fits
krishhgg Oct 2, 2026
4e08846
Config: migrate only files from older builds, so a 30-day ceiling set…
krishhgg Oct 2, 2026
a7464d9
Merge branch 'main' into fix/out-of-process-cutoff
krishhgg Oct 2, 2026
7707b3e
Tests: hold the fake app's alive lock until the test releases it
krishhgg Oct 2, 2026
327682a
Backstop: run the battery and thermal reads without the recovery lock
krishhgg Oct 2, 2026
eef24dc
Backstop: remove session.json as soon as a run ends a valid session
krishhgg Oct 2, 2026
884ff01
Docs: describe the duration tooltips without en dashes
krishhgg Oct 2, 2026
b9cd044
Backstop: record an end whose session.json cannot be removed
krishhgg Oct 2, 2026
242e4e9
Tests: run the agent-end retry ticks one at a time
krishhgg Oct 2, 2026
5a9b68e
Backstop: send timeout signals through $KILL
krishhgg Oct 2, 2026
87fe970
Merge origin/main into fix/out-of-process-cutoff
krishhgg Oct 2, 2026
9bbfdc9
Session: settle a pending end whichever path finishes it
krishhgg Oct 2, 2026
ecf7db4
Tests: check the read's descriptors outside the timed window
krishhgg Oct 2, 2026
42f369d
Backstop: report an end record that cannot be removed
krishhgg Oct 2, 2026
cbe8cdf
Docs: describe what a reboot does to a session still in the future
krishhgg Oct 2, 2026
d46a6fe
Merge origin/main into fix/out-of-process-cutoff
krishhgg Oct 3, 2026
e8c749d
Backstop: call rm and mv through the path variables
krishhgg Oct 3, 2026
a676549
Store: read the end record and session.json only as regular files
krishhgg Oct 3, 2026
3c302ba
Backstop: read the battery and the end floor the way the app does
krishhgg Oct 3, 2026
6d99b72
Config: keep an undecodable config.json instead of writing defaults o…
krishhgg Oct 3, 2026
d245a97
App: run only as the copy that holds the alive lock
krishhgg Oct 3, 2026
cadfe0c
Merge origin/main into fix/out-of-process-cutoff
krishhgg Oct 3, 2026
12417c4
Tests: put INSOMNIA_HOME back instead of unsetting it
krishhgg Oct 3, 2026
784a080
Backstop: open the end record, config.json and the log only as regula…
krishhgg Oct 3, 2026
d76d4e5
Merge origin/main into fix/out-of-process-cutoff
krishhgg Oct 3, 2026
4a13512
Tests: run the offset-date backstop test with the app alive
krishhgg Oct 3, 2026
906b5d2
Merge origin/main into fix/out-of-process-cutoff
krishhgg Oct 3, 2026
bda4dce
Security: holding the liveness lock bypasses only the app check
krishhgg Oct 3, 2026
91e6d3c
Config: run no session on a config.json the app rejected and kept
krishhgg Oct 3, 2026
5e00cb2
Merge origin/main into fix/out-of-process-cutoff
krishhgg Oct 6, 2026
0c4a3b7
Config: keep refusing sessions until the replacement config.json is w…
krishhgg Oct 6, 2026
49261fb
Merge origin/main into fix/out-of-process-cutoff
krishhgg Oct 6, 2026
64d8589
Merge origin/main (aed25a5: #22, #28, #49) into fix/out-of-process-cu…
krishhgg Oct 7, 2026
5128f51
Merge origin/main (781b596, #33) into fix/out-of-process-cutoff
krishhgg Oct 7, 2026
7a05ff6
Config: write the settings in use where a rejected file that could no…
krishhgg Oct 7, 2026
3e5dd12
Tests: compare a migrated older file with an earlier build's lid sett…
krishhgg Oct 7, 2026
de6131d
Tests: line up main's #19 and #22 tests with the alive lock and the e…
krishhgg Oct 7, 2026
9e86194
Tick: back off after any refused agent-end transaction, not only a bu…
krishhgg Oct 7, 2026
f14c6ca
Merge origin/main (bfc9a57, #50) into fix/out-of-process-cutoff
krishhgg Oct 7, 2026
b72a4f6
Backstop: run each read as this shell's own job, on #50's clock and c…
krishhgg Oct 7, 2026
9a24156
Launch: watch output devices only once this copy holds the alive lock
krishhgg Oct 7, 2026
98dba00
Settings: the app and the agent enforce the same end floor and therma…
krishhgg Oct 7, 2026
7cede36
Ended session: record the end in the journal when ended-session.json …
krishhgg Oct 7, 2026
7bab185
Docs: describe the shared cutoff policy and the journaled end record
krishhgg Oct 7, 2026
e52ea07
Tests: set the fake sudo's watchdog deadline before it reports its pid
krishhgg Oct 8, 2026
be4b4c8
Tests: fail the App Nap journal write after the resume's own write
krishhgg Oct 8, 2026
b96f62a
Tests: let the fake sudo wait on SECONDS so one signal is logged once
krishhgg Oct 8, 2026
953b4b0
Reconcile: end a session whose journaled sleep hold reads 0; clamp en…
krishhgg Oct 8, 2026
bf71148
Record an end aside when no other file takes it; never read a rejecte…
krishhgg Oct 8, 2026
252557d
Backstop: read the cutoffs through the app's decoder; record an end i…
krishhgg Oct 8, 2026
bec766b
Record an end in the recovery lock file and journal the session's cut…
krishhgg Oct 8, 2026
7a28ce0
Merge origin/main b5f7cf0 (PR #43) into fix/out-of-process-cutoff
krishhgg Oct 8, 2026
2acc91d
Tests: a session that runs on after a restart in main's #43 claim tes…
krishhgg Oct 8, 2026
1e1178b
Tests: shorten the fake command limit where no status file can be wri…
krishhgg Oct 8, 2026
958ba1a
Scripts: read state only through fixed tool paths, never PATH
krishhgg Oct 8, 2026
844947d
Agent: keep a session on a journal the app cannot load; end on a fail…
krishhgg Oct 8, 2026
277b77f
Scripts: pass ShellCheck on the journal check and the agent-app list
krishhgg Oct 8, 2026
47bdc6c
Scripts: keep uninstall.sh's dirname and bounded() lines as install.s…
krishhgg Oct 8, 2026
277b62a
Round 29: record an end in insomnia.log, check what the app decodes, …
krishhgg Oct 8, 2026
f2298fe
Lock record: count this session's record cut short as its end, never …
krishhgg Oct 8, 2026
159a570
Round 31: settle the lock file at start, keep log lines apart, read c…
krishhgg Oct 9, 2026
a41341c
Round 31: policy tables expect no restore without a journal, a record…
krishhgg Oct 9, 2026
1e4e1d4
Round 33: every log writer holds flock(2) on insomnia.log, so no line…
krishhgg Oct 9, 2026
b930a8a
Round 33: the scripts read the journal and config.json as the app doe…
krishhgg Oct 9, 2026
5182db6
Docs: describe the scripts' own readers, the lock file reads and the …
krishhgg Oct 9, 2026
1598bd4
Tests: a journal with a key written twice ends the session, as the ap…
krishhgg Oct 9, 2026
0b42d74
Tests: four independent script tables run several rows at a time, and…
krishhgg Oct 9, 2026
62d57c5
Tests: the cut-short and bad-escape configs run again with every way …
krishhgg Oct 9, 2026
b25db3d
Scripts: a journal published from a view keeps the Floats and escaped…
krishhgg Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 72 additions & 16 deletions .greptile/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,39 @@ Before the backstop's `lowpowermode 0`, when the journal has
the app doubts that entry's readings in that boot even if the journal of
the undo never lands. If that publish fails, the mode is left on and its
entry kept for retry. Both scripts check the records' types and read the
three keys from the file's text too (`record_text_problems`, the same in
both), as the app's decoder reads it: keys of the top-level object only,
with their `\u` escapes decoded, every value stepped over whole, so a
string or nested value holds no record. Each number must be null or a
JSON number that a Swift Float holds and that does not round to 0 from a
nonzero value (plutil turns 1e-400 into 0.0). One of the keys found twice
at the top level, a key with an escape JSON does not have, and a top
level the reader cannot follow (JSON5 keys, comments, NUL bytes as in
UTF-16) are refused. Any of
these makes the journal malformed, so nothing is undone and uninstall
removes nothing. `savedAudioOutputs` entries alone leave the
whole file's text too (`record_text_problems`, the same in both), as the
app's decoder reads it: every object and array at any depth, keys with
their `\u` escapes decoded (a Kelvin sign read as K), strings stepped over
whole, so a saved name holds no key. A key written twice counts by its
first copy, as in the app's decoder. A number where the app reads a Float
must not round to infinity, or to 0 from a nonzero value, compared as
exact decimal digits rather than through plutil's Double; one where it
reads an Int32 or Int64 must be a whole number the type holds as the app
reads it (`5105.0`, `1e3`, `1e-400` as 0). Where plutil would read the
file otherwise than the app (a later copy of a key, an escaped key, such a
number, UTF-16 or UTF-32), the scripts read and edit a view of the journal
as the app reads it, which drops what the app's own save drops. An escape
JSON does not have, a value that is no JSON value and text the app's
decoder refuses (JSON5 keys, comments) are refused. So are two forms the
app loads but never writes, a NUL byte and `\u0000` in a string the app
reads, and text the reader does not finish within 30 s (very many keys or
a very long array). Any of these makes the journal malformed, so nothing
is undone and uninstall removes nothing. Values are checked only where the
app reads them: in a frozen process, `startedAtMicros` only after a
`startedAt` that is there and not null, and `bootSession` only after both,
as `FrozenProcess` decodes them. A `sessionCutoffs` the app reads as no
record (not a string of the form it writes) is a record the app does not
write (read as none), not a malformed journal. When the app's binary cannot
answer for `config.json`, backstop.sh reads that file with the same reader
in its config form, which also checks each value's type (`config_cutoffs`),
and uses it only when the reader finds nothing it cannot settle: not a file
over 8 MiB, one it does not finish within 30 s, or one on which Foundation
stops the app. So a hand edit reaches the backstop on that path only in a
form the reader reads as the app does. backstop.sh
checks the journal so before it reads the cutoffs for a valid session or
ends one: a journal that fails, or that `--agent-session-cutoffs` answers
`rejected` for, stops the run with `session.json`, the journal and every
undo entry kept. `savedAudioOutputs` entries alone leave the
journal clean for the backstop (an entry can wait days for its device), but
uninstall stops on them. A saved brightness flagged
`displayRestoreRefused` or `keyboardRestoreRefused` (the app's private-call
Expand Down Expand Up @@ -86,11 +108,42 @@ known key of the wrong type is left untouched and the run exits 1.

The app and the script serialize on one `flock(2)` lock,
`.recovery.lock`, which is never unlinked so both lock the same inode
(`RecoveryLock.swift`; `lockf` on fd 9 in the scripts). `uninstall.sh`
(`RecoveryLock.swift`; `lockf` on fd 9 in the scripts). It may also hold a
record of a session's end (`ended-session-v1 <base64>`), written in place
through the held descriptor and never by replacing the file. Both writers
cut the file back to bytes it shares with the start of the record, or to
nothing, before they append the rest, so a stop partway never leaves the
record's first bytes over old bytes that differ. That record cut short as
a writer leaves it (its first bytes, or the whole record with the file's
old bytes after it) counts as the end of the session whose bytes it starts
with, and no writer empties it; other content that is no record ends
nothing, and the app empties it before a resumed session goes on. A start
settles the file before it writes session.json
(`Store.settleLockForStart`): a stale record's first bytes would end the
new session too. An unreadable lock file is never written over. Both sides
read it three times, 0.1 s apart (`Store.lockReadAttempts`,
`LOCK_READ_ATTEMPTS`), before it counts as unreadable, and then it counts
as the end of the session in session.json: a rule for the safe side, not
proof of an end, since content that is no record but keeps failing to read
ends a live session. When insomnia.log holds that session's record, the log
is named as where the end is recorded. When the lock file write fails too,
the end is appended to
`insomnia.log` as one line (`insomnia-ended-session-v1 <size> <base64>`,
`LogEndRecord.swift`). Every writer of the log in this repo (the app's
`OwnerOnly.appendToLog` and `LogEndRecord`, backstop.sh's `log` and
`record_end_in_log`, the LaunchAgent program) holds flock(2) on the log
from its look at the last byte until its write ends and puts a newline
first when the log does not end in one, so no line of theirs joins a
record; a process that appends without that lock still can. insomnia.log
is rotated only under the recovery lock and the log's flock, with a record
still in force copied forward. `uninstall.sh`
takes the lock, runs the backstop with `--force` under it, and refuses to
remove the recovery machinery while anything is still journaled. Battery
and thermal floors run only while the app is alive; the backstop does not
provide them.
remove the recovery machinery while anything is still journaled. The Low
Power Mode requests for battery and thermal run only while the app is
alive. The ends do not: the backstop ends a valid session below the end
floor on battery power, on a battery it cannot read and at critical
thermal pressure, once a minute, as well as at its deadline and once the
app is gone.

## Deliberate designs, do not flag

Expand Down Expand Up @@ -179,7 +232,7 @@ Flag a change that breaks one of these; do not flag the behavior itself.
except that a backstop run gives `keptDisplayUnderLowPowerBoot` its own
boot, in a journal it publishes before its `lowpowermode 0`, and leaves
the mode on if it cannot. The records are read from the file's text as
well as through plutil (`record_text_problems`), at the top level only.
well as through plutil (`record_text_problems`), which reads every object.
Legacy `frozenPids` are never signaled or cleared there, even when the
pid is gone (spec section 8).
- `ProcessControl.swift`, `LidActions.swift`, `backstop.sh`. Only pids
Expand Down Expand Up @@ -220,6 +273,9 @@ Flag a change that breaks one of these; do not flag the behavior itself.
`insomnia.log`, a local file shared with `backstop.sh` so one file tells
the whole story. That file may contain SSIDs, process metadata and tmux
target names (SECURITY.md). The privacy rule applies to the unified log.
A line written without the recovery lock is never followed by a rotation
(`OwnerOnly.LogRotation.deferred`), so the file can pass 1 MiB until a
line is written under the lock: it may hold an end record.
- `LidActions.swift`. Lid events do nothing when no session is active
(spec section 3).
- `simulate-lid.sh`, `LidSimulation.swift`. The trigger file is the same
Expand Down
Loading
Loading