Repository navigation
fix(daemon): read a macOS daemon start time in UTC, like the agent-session probe - #272
Merged
nplez1 merged 4 commits intoNov 2, 2026
Conversation
…ssion probe `ps -o lstart=` prints local wall clock, and the hour a DST fall-back repeats resolves to the earlier offset, so for one hour a year a daemon start time read back exactly an hour early. #268 fixed that in the agent-session probe; these daemon paths still had it, and neither value is compared against itself — both meet a recorded epoch, so the error does not cancel. - `readMacosProcessStartedAtMs` feeds `probeMacosProcess`, whose start-time evidence then fails to match the incarnation it is verifying, degrading evidence to `unknown`. - `getPsProcessIdentityAsync` feeds the PID-identity and retired-daemon-recovery comparisons, which use it to prove the daemon is gone. - `getProcessStartedAtMs` reached the same defect through the synchronous twin's default, so the zone is now pinned unconditionally and the `utc` option is gone rather than left as a wrong default. The two idioms #268 added move into `src/shared/ps-lstart-time.ts` so all three call sites read one implementation instead of three copies. The identity query now inherits the en_US.UTF-8 locale upstream pinned for macOS ps, which keeps non-ASCII argv readable in the command line it matches on.
… UTC read still matches Round 1 (Opus, high) found one blocking defect in R1: `macos-retired-daemon-recovery` compared `Date.parse(snapshot.root.startedAt)` — the descendant table's local wall clock — with the daemon identity's epoch, which R1 had just made UTC. Before R1 both sides were local and matched (wrong by the same hour); after it, a daemon started in the repeated hour differed by 3 600 000 ms and recovery threw "Could not verify the retired macOS terminal service". One zone now covers every lstart-to-epoch conversion, which is what makes the comparison coherent again rather than dropping it: - the fresh descendant table and the provider row reader ask ps for lstart with TZ=UTC, next to the C locale they already pin; - `hasUnambiguousStartTime` and the recovery comparison read that text with `parseUtcLstart`, so the second-boundary PID-reuse proof is also right in the hour that repeats, instead of reading a start an hour into the future. Round 1's other two findings: the repeated-hour guards now stub TZ to America/Los_Angeles, because on a UTC runner a local read satisfies them and the guard would be vacuous (the shared test asserts that premise, so the guard cannot quietly stop biting); and a live darwin test reads this process's own start time with TZ stubbed, which fails if either the in-process reader or the ps fallback loses the zone pin — verified by reverting the env and watching it red. Recorded start times are noted in the PR body: a daemon or orcad that started in the repeated hour and is still alive across the upgrade can mismatch by one hour once.
…ay the code does Round 2 (Opus, high) found that nothing tested the two `TZ: 'UTC'` pins R2 added to the ps readers, and that `daemon-tcc-attribution-main-thread.test.ts` was red — an R1 miss it caught from the delta. The pins are safety-relevant, not cosmetic: a local `lstart` text parsed as UTC on a Mac west of UTC reads every row hours older, so `hasUnambiguousStartTime` clears a start it cannot distinguish from PID reuse and SIGKILL can reach a recycled PID. Dropping either pin failed no test on any runner. Each reader now has a guard that fails when its pin is removed — the fresh descendant table asserts the `ps` env, and the provider row reader reads this process's own marker with `TZ` stubbed and checks it parses to the real start instant. Both verified by removing the pin and watching them red. The tcc attribution fixture wrote a pid record from a local `Date.parse` of the same `lstart` text its `ps` mock returns, which the daemon identity now reads as UTC, so the 1 500 ms comparison became a 7-hour mismatch on this machine and the suite was red here while green on UTC CI. It and one leftover fixture now use `parseUtcLstart`.
…te string The unit gate caught this one: the mock emitted `new Date(1_700_000_000_000).toString()`, which carries a GMT offset and the zone name, so it is not the fixed-width lstart that `ps -o lstart=` prints. `Date.parse` accepted it; `parseUtcLstart` reads the first 24 characters and correctly refuses it, so the evidence fell to `unknown`. The fixture now emits the lstart for that same instant.
nplez1
deleted the
nplez1/fix-daemon-read-a-macos-daemon-start-time-in-utc
branch
November 2, 2026 09:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
Fixes #269.
ps -o lstart=prints local wall clock, and the hour a DST fall-back repeats resolves tothe earlier offset, so for one hour a year a daemon start time read back exactly one hour early.
#268 fixed this in the agent-session probe; the daemon paths still had it, and neither value is
compared against itself — both meet a recorded epoch, so the error does not cancel:
readMacosProcessStartedAtMsfeedsprobeMacosProcessindaemon-incarnation-evidence.ts, where anhour of error turns "this is our daemon" into
process_start_timeevidence that does not match, soincarnation evidence degrades to
unknown.getPsProcessIdentityAsyncfeedsdaemon-pid-identity.tsandmacos-retired-daemon-recovery.ts,where the identity captured before a kill is compared with the one read after it to prove the daemon
is gone.
Measured on 2026-11-01 (the US fall-back date) for a process started at 01:37 PST:
The fix — one zone for every
lstart→epoch conversionpsis asked forlstartwithTZ=UTCand the text is read as UTC. The two idioms #268 added movefrom the agent-session probe into
src/shared/ps-lstart-time.ts(psUtcEnvironment,parseUtcLstart),so every call site reads one implementation instead of three copies.
runPsInProcessformats its ownrows for the env it is given, so the in-process reader and the
/bin/psfallback agree.readMacosProcessStartedAtMsparses withparseUtcLstart, andrunInspectionCommandhandspsUtcEnvironment()to both readers.getPsProcessIdentity'sutcoption is gone rather than defaulted: that fixed
getProcessStartedAtMs(
daemon-process-start-time.ts:66), a third call site with the same defect the issue did not list. Awrong default is the shape this bug keeps returning in.
lstarttext pinTZ=UTCnext to the C locale they already pinned, andhasUnambiguousStartTimereads that text withparseUtcLstart. Without this the recovery comparison below, and the second-boundary PID-reuse proofthat gates SIGKILL escalation, would be reading a local string as UTC.
macos-retired-daemon-recoverycomparedDate.parse(snapshot.root.startedAt)withthe daemon's epoch; the two sides are now in the same zone, so the check keeps proving the snapshot's
root row is the daemon instead of being dropped.
The identity query also inherits the en_US.UTF-8 locale upstream pinned for macOS ps (stablyai#33898) instead
of overriding it with
LC_ALL=C, so thecommand=half of the identity is no longer vis-encoded fornon-ASCII paths.
String-vs-string
lstartcomparisons (managed-hook-owner-identity.ts,mac-browser-sidecar-recovery.ts,pty-descendant-termination.ts's live-row match) compare two readsof the same process in the same zone, so the zone cancels and they are deliberately untouched.
What the review changed
Round 1 caught the defect the first version introduced: pinning the daemon identity to UTC left
macos-retired-daemon-recoverycomparing that epoch with the descendant table's locallstarttext.Both sides used to be local, so they agreed; after the fix a daemon started in the repeated hour
differed by 3 600 000 ms and recovery threw. That is why this PR ends up pinning the descendant and
provider readers too, instead of dropping the comparison.
Round 2 (delta) found that nothing guarded the two new reader pins — dropping one turned the
PID-reuse proof into "always unambiguous" on a Mac west of UTC, with no test red — and that
daemon-tcc-attribution-main-thread.test.tswas already red on this machine from round 1, because itsfixture built a pid record from a local
Date.parseof the same text itspsmock returns. Both arefixed with tests that fail when the pin or the parse is reverted, each verified by reverting it.
The full unit gate then caught one more fixture of that shape —
daemon-incarnation-evidence-main-threadmocked
pswithnew Date(...).toString(), which carries a GMT offset and is not the fixed-widthlstartps prints;Date.parseaccepted it andparseUtcLstartcorrectly does not.Known one-time window
A start time recorded by the previous build (in a daemon PID file,
orcad's record, or profilestate) was parsed local and is compared within a tolerance against the new UTC reads. A daemon or
orcadthat started in the repeated hour of a fall-back day and is still alive across the upgrade canmismatch once, by one hour. The window is one hour, twice a year, and it degrades to "unknown" identity
rather than to a wrong kill; no migration code is worth it, and this is recorded here rather than
silently absorbed.
Verification
src/shared/ps-lstart-time.test.ts— the repeated-hour string parses toDate.UTC(2026, 10, 1, 1, 37, 4), withTZstubbed toAmerica/Los_Angelesand a premise assertionthat a local read of the same string cannot yield it, so the guard bites on a UTC runner too instead
of only on a non-UTC developer machine.
src/main/daemon/daemon-process-inspection.test.ts— the same guard through the daemon reader, plusa live darwin case that reads this process's own start time with
TZstubbed; it fails if either thein-process reader or the
/bin/psfallback loses the zone pin (verified by reverting the env andwatching it red).
src/main/daemon/daemon-process-identity-time-zone.test.ts— both twins assert the UTC parse, thatpsgetsTZ=UTC, and that the async twin passes the same environment as the synchronous one.daemon-health,daemon-bundle-staleness,macos-retired-daemon-recovery,profile-state-access-identity,agent-session-process-identity-probe,pty-descendant-termination(+-libproc),pty-descendant-exit-verification,provider-process-row-read,provider-group-orphan-guard(+integration),managed-provider-process(+fallback-tree),
terminal-descendant-shutdown(+-libproc),claude-agent-sdk-exit-proof(+
-identity,-root-kill-fallback),claude-descendant-escalation-boundary, and the threestructured-agent-session-*-kill-evidencesuites.pnpm tc:nodeandpnpm tc:webclean;pnpm run check:code-quality:changed nplez1/mainclean;node local/check-upstream-file-comments.mjsat baseline (6026/6026).Reviewed by Opus 5.5, high: R1 (1 blocking fixed), delta on R2 (2 blocking fixed)
src/shared/):Unit gate @3d442589e9 (clean): PASS — 135050 passed; 0 failed (0 allowlisted, 0 unexpected); 0 flaky re-run passed; 625 s