fix(conduit): make capture, rollup and scheduling work off macOS - #1788
Open
pkumaschow wants to merge 2 commits into
Open
fix(conduit): make capture, rollup and scheduling work off macOS#1788pkumaschow wants to merge 2 commits into
pkumaschow wants to merge 2 commits into
Conversation
…from sessions
Conduit's app-focus adapter shells out to `osascript`, which only exists on
macOS. On every other platform `execFileSync` throws straight into a bare
`catch { return [] }`, so the adapter emits nothing and says nothing.
That is invisible rather than merely degraded, because rollup.ts derives
totalMinutes, creationMinutes, consumptionMinutes and every block exclusively
from app-focus events. Commits and claude-sessions are counted but contribute
no time. The result on a Linux host is a dashboard that reports the capture job
healthy, accumulates events all day, and renders an empty timeline — with no
log line anywhere explaining why.
Two changes:
1. appFocus.ts checks the platform before shelling out and logs the reason
once per process. The `catch` also reports now: on macOS a throw there means
a real fault (Automation permission denied, System Events unresponsive)
rather than an expected platform gap, and that deserves to be audible too.
Once per process, not per poll — a frequent cron would drown the log.
2. rollup.ts falls back to deriving time from claude-session cadence when a day
has no focus time at all. Each session event is credited with the gap back to
the previous one, capped so an overnight gap is not counted as work. The cap
self-tunes to 2x the day's median gap rather than a fixed constant, because
the capture cadence comes from the cron schedule rather than pollIntervalSec,
so a hardcoded value would silently undercount if that schedule changed.
The fallback engages only when there is no measured focus time, so macOS
behaviour is unchanged. Derived time is labelled as derived in the block itself
rather than blended into the same number, because inferred keyboard time and
measured focus time are not the same claim.
There is no drop-in app-focus replacement on Wayland: an unprivileged process
cannot ask which window has focus. GNOME's
org.gnome.Shell.Introspect.GetWindows returns AccessDenied, and xprop sees only
XWayland clients, so it would report confidently wrong apps for native ones. A
Shell extension exposing focus over D-Bus is the real port; this change makes
the gap visible and keeps the timeline useful until then.
… macOS BuildInsight.ts is scheduled exclusively by InstallConduitInsight.ts, which writes ~/Library/LaunchAgents/com.lifeos.conduit.insight.plist. launchd only exists on macOS, so on every other platform nothing ever runs it — while the Conduit page advertises an "hourly read" and permanently displays "No activity captured yet today". Pulse is the cross-platform scheduler LifeOS already ships, so the job belongs there. The launchd agent still works where it is installed; this just means the hourly read is real everywhere else. Double-running is harmless where both are active. BuildInsight is idempotent against a `since` watermark and exits WITHOUT an inference call when no new events have landed, so extra firings on idle hours cost nothing. Verified by running it twice in succession: the second invocation reported "no new events since <watermark> — skipped (no inference call)". Schedule is :23 past every second hour. The minute was brute-forced against every enabled job in the shipped config rather than eyeballed — the obvious :15 collides with five of them, because cost-aggregation (*/15) and the */5 pollers all land on multiples of 5. This job holds an inference call open for roughly 50 seconds, so it should not share a tick with anything.
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.
Three related macOS assumptions in Conduit that leave it visibly broken on other platforms, each failing silently. Two commits: the capture/rollup fix, and the scheduling fix.
Problem 1 — app-focus fails silently, taking the whole time model with it
Conduit's
app-focusadapter shells out toosascript, which only exists on macOS. On any other platformexecFileSyncthrows straight into a barecatch { return [] }— the adapter emits nothing and says nothing.That's invisible rather than merely degraded, because
rollup.tsderivestotalMinutes,creationMinutes,consumptionMinutesand every block exclusively fromapp-focusevents. Commits andclaude-sessionevents are counted but contribute no time.On a Linux host the result is a dashboard that looks healthy from every angle you'd normally check:
okclaude-session+ 39git-commitover 18 days)/api/conduitresponds with sensiblecommitsandsessionscountstotalMinutes: 0, with zeroapp-focusevents ever recorded and no log line anywhere explaining whyChanges
1.
adapters/appFocus.ts— make the failure audiblePlatform-check before shelling out, and log the reason once per process:
The
catchreports now too. On macOS a throw there is a real fault — Automation permission not granted, System Events unresponsive — not an expected platform gap, so it deserves to be audible as well. Once per process rather than per poll, since a frequent cron would drown the log.2.
rollup.ts— derive time from the signal that exists everywhereWhen a day has no focus time at all, time is derived from
claude-sessioncadence. Each session event is credited with the gap back to the previous one, capped so an overnight gap isn't counted as work.The cap self-tunes to 2× the day's median gap rather than a fixed constant: the capture cadence comes from the cron schedule, not
pollIntervalSec, so a hardcoded value would silently undercount the moment that schedule changed.Problem 2 — the insight job is scheduled by launchd only
BuildInsight.tsis scheduled exclusively byInstallConduitInsight.ts, which writes~/Library/LaunchAgents/com.lifeos.conduit.insight.plist. launchd only exists on macOS, so everywhere else nothing ever runs it — while the Conduit page advertises an "hourly read" and permanently shows "No activity captured yet today".3.
PULSE.toml— schedule it through the scheduler LifeOS already shipsThe launchd agent still works where it's installed; this just makes the hourly read real on every other platform. Double-running is harmless —
BuildInsightis idempotent against asincewatermark and exits without an inference call when no new events have landed. Verified by running it twice in succession; the second printedno new events since <watermark> — skipped (no inference call).The minute was brute-forced against every enabled job in the shipped config rather than eyeballed. The obvious
:15collides with five of them —cost-aggregation(*/15) and the*/5pollers all land on multiples of 5 — and this job holds an inference call open for ~50s, so it shouldn't share a tick with anything.:23is clear across all of them.Design notes
osascriptnever takes this path.Claude Code (derived from session cadence)rather than being blended into an unqualified number. Inferred keyboard time and measured focus time aren't the same claim and the record shouldn't blur them.Why not just port app-focus to Linux?
There's no drop-in replacement on Wayland — an unprivileged process deliberately cannot ask which window has focus. I tested both routes on Fedora/GNOME:
org.gnome.Shell.Introspect.GetWindowsAccessDenied: GetWindows is not allowedxprop -root _NET_ACTIVE_WINDOWxdotool/wmctrlA GNOME Shell extension exposing focus over D-Bus is the real port. This PR makes the gap visible and keeps the timeline useful until someone writes it.
Verification
I didn't add a test file — the repo has no test infrastructure (no root
package.json, no.test.tsfiles), and I didn't want to impose a framework in a bugfix PR. Happy to add one if you'd like it. The fallback was verified against a 10-case matrix, all passing:creationEnd-to-end on a Fedora 42 / GNOME / Wayland host: Conduit went from
totalMinutes: 0with an empty timeline to189.2minutes across one labelled derived block, with commits and sessions counts unchanged.For the scheduling commit: Pulse loaded the job (15 → 16 jobs), the loader resolves it as
schedule=23 */2 * * * type=script output=log enabled=true, and a full 24-hour minute sweep against all other enabled jobs returns zero collisions. The insight itself produced a correct read from 33 events on first run, and skipped without inference on the second.