Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -946,7 +946,15 @@ explicitly makes it read `~/.claude/.claude.json`, a file nothing else
uses. `connect` therefore addresses the default directory by *unsetting*
the variable. And launchd gives an agent 256 open files by default; the
daemon exhausted that within an hour and fell back to archive snapshots for
inbox reads, so the plist sets 4096.
inbox reads, so the plist sets 4096. That only buys time: `am` 0.3.36 leaks
sqlite handles on its store (234 of 258 after five days, first seen
2026-09-29), and a plist written before the limit existed keeps launchd's
256 until `postbox install` rewrites it. `status` therefore prints a `files:`
line — open handles against the *installed* plist's limit, naming a stale
plist and warning past 80%. `install` waits for `bootout` to finish before
`bootstrap`: launchd tears the job down after bootout returns, and a
bootstrap in that window fails "5: Input/output error" and leaves nothing
loaded.

Commands: `name [--source] [dir]`, `names [root...]`, `install`, `uninstall
[--purge]`, `start|stop|restart|status|logs`, `connect|disconnect`, `hook
Expand Down
17 changes: 17 additions & 0 deletions bin/executable_postbox
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ cmd_install() {
mkdir -p "$HOME/Library/LaunchAgents"
write_plist
launchctl bootout "gui/$(uid)/$LABEL" >/dev/null 2>&1
# bootout returns before launchd has torn the job down; a bootstrap in
# that window fails "5: Input/output error" and leaves nothing loaded.
i=0
while launchctl print "gui/$(uid)/$LABEL" >/dev/null 2>&1 && [ $i -lt 50 ]; do
sleep 0.1; i=$((i + 1))
done
launchctl bootstrap "gui/$(uid)" "$(plist_path)" || die "launchctl bootstrap failed (see $(plist_path))"
launchctl kickstart -k "gui/$(uid)/$LABEL" >/dev/null 2>&1
printf 'postbox: daemon registered as %s on %s\n' "$LABEL" "$(url)"
Expand Down Expand Up @@ -887,6 +893,17 @@ cmd_status() {
if launchctl print "gui/$(uid)/$LABEL" >/dev/null 2>&1; then
pid=$(launchctl print "gui/$(uid)/$LABEL" 2>/dev/null | awk '/^\tpid = / { print $3; exit }')
printf 'daemon: %s loaded%s\n' "$LABEL" "${pid:+ (pid $pid)}"
# am leaks sqlite handles (~1 per half hour on 0.3.36), so the
# headroom is worth watching. The limit is what the INSTALLED plist
# says — a plist from before the limit existed runs on launchd's 256.
if [ -n "$pid" ]; then
open=$(lsof -p "$pid" 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')
lim=$(grep -A3 '<key>SoftResourceLimits</key>' "$(plist_path)" 2>/dev/null | sed -n 's:.*<integer>\([0-9]*\)</integer>.*:\1:p')
printf 'files: %s open of %s' "$open" "${lim:-256}"
[ -z "$lim" ] && printf ' (launchd default: the installed plist predates the limit — postbox install)'
[ "$open" -ge $(( ${lim:-256} * 4 / 5 )) ] && printf ' — WARNING: over 80%%; postbox restart'
printf '\n'
fi
else
printf 'daemon: %s not loaded (postbox install)\n' "$LABEL"
fi
Expand Down
42 changes: 42 additions & 0 deletions tests/postbox.bats
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,48 @@ STUB
[[ "$output" == *"NOT answering"* ]]
}

@test "install: waits for bootout to finish before bootstrapping again" {
# launchd keeps the old job for a moment after bootout returns; a
# bootstrap in that window fails with error 5 and leaves nothing loaded.
echo 3 > "$BATS_TEST_TMPDIR/still-loaded"
cat > "$BIN/launchctl" <<'STUB'
#!/bin/sh
printf '%s\n' "$*" >> "$LAUNCHCTL_LOG"
f="$BATS_TEST_TMPDIR/still-loaded"
if [ "$1" = print ]; then
n=$(cat "$f"); [ "$n" -gt 0 ] || exit 1
echo $((n - 1)) > "$f"; exit 0
fi
exit 0
STUB
run "$PB" install
[ "$status" -eq 0 ]
# bootout, three prints that still see the job, the one that does not, then bootstrap.
[ "$(grep -o '^[a-z]*' "$LAUNCHCTL_LOG" | tr '\n' ' ')" = "bootout print print print print bootstrap kickstart " ]
}

@test "status: open files against the installed plist's limit, and a stale plist is named" {
cat > "$BIN/launchctl" <<'STUB'
#!/bin/sh
[ "$1" = print ] && printf '\tpid = 4242\n'
exit 0
STUB
cat > "$BIN/lsof" <<'STUB'
#!/bin/sh
echo HEADER; i=0; while [ $i -lt "$FAKE_FDS" ]; do echo fd; i=$((i + 1)); done
STUB
chmod +x "$BIN/launchctl" "$BIN/lsof"
"$PB" install >/dev/null
FAKE_FDS=40 run "$PB" status
[[ "$output" == *"files: 40 open of 4096"$'\n'* ]]
FAKE_FDS=3300 run "$PB" status
[[ "$output" == *"files: 3300 open of 4096 — WARNING: over 80%; postbox restart"* ]]
# A plist written before the limit existed runs on launchd's default.
sed -i.bak '/ResourceLimits/,/<\/dict>/d' "$HOME/Library/LaunchAgents/local.postbox.plist"
FAKE_FDS=261 run "$PB" status
[[ "$output" == *"files: 261 open of 256 (launchd default: the installed plist predates the limit — postbox install) — WARNING"* ]]
}

@test "status: an installed version other than the pinned one is a nudge to re-check the vocabulary" {
cat > "$BIN/am" <<'STUB'
#!/bin/sh
Expand Down
Loading