Conduit

Conduit — LifeOS's Sensory Layer

Last synced: Jul 11, 2026

Conduit — LifeOS’s Sensory Layer

Conduit is the “current-state” pole of the current→ideal loop. LifeOS has always known your ideal state (TELOS) but not your actual state — what you do day to day. So it could not answer “are we working on the right stuff?” Conduit gives LifeOS eyes: a local, continuous, opt-in capture layer that records where attention actually goes, rolls it into a daily record, and feeds that record to the memory and TELOS systems. Design ISA: LIFEOS/MEMORY/WORK/20260704-conduit-context-gathering/ISA.md.

Component version: Conduit v1.0.0 · shipped in LifeOS 6.1.0.

Design principles

  • A mirror you pull, not a watcher that pushes. Conduit hands you your own attention back; it never volunteers a verdict and never produces a single alignment “score” (that would just invite gaming — Meadows LP3). Distribution + record, you judge.
  • Perception feeds the existing loop. Conduit is one new input that makes memory curation, WorkSweep, TELOS state, and Pulse smarter — not a second dashboard.
  • Highest signal tier per source; watch the account, not the window. Where a source has an API (Gmail, Calendar, GitHub), read the account, not the app’s pixels.
  • Stable by construction. v1 is deterministic — no model, no cloud, no long-lived daemon. Stateless launchd polls, fault-isolated adapters, a pure rollup.
  • Privacy is absolute. All data under USER/, on this machine, never leaves it.

Where things live (the hard boundary)

KindLocation
Code (system area)LIFEOS/PULSE/Conduit/ + Pulse module LIFEOS/PULSE/modules/conduit.ts
Data (USER)LIFEOS/USER/CONDUIT/config.json, state.json, events/<date>.jsonl, daily/<date>.{md,json}, logs/

Only code lives in the code area; every byte Conduit captures lives under USER/.

Architecture

launchd (com.lifeos.conduit, every 120s)
      │  conduit capture

┌─────────────── adapters (fault-isolated) ───────────────┐
│ appFocus (osascript front app)                          │
│ git (new commits across configured repos)               │
│ claudeSession (reads MEMORY/STATE/work-events.jsonl)    │
└──────────────────────┬──────────────────────────────────┘

        USER/CONDUIT/events/<date>.jsonl  (append-only raw)
                       │  (day rolls over)

        rollup.ts  →  deterministic DailyRecord

        USER/CONDUIT/daily/<date>.{md,json}

        ┌──────────────┼───────────────┬──────────────┐
        ▼              ▼               ▼              ▼
   Pulse module   memory system   TELOS/LIFEOS_STATE  WorkSweep
   (/api/conduit) (daily context) (observed vs ideal) (untracked work)

Event schema

One JSONL line per captured signal. Spans + metadata only — never keystrokes or content.

interface ConduitEvent {
  ts: string;                    // ISO-8601 UTC
  type: "app-focus" | "git-commit" | "claude-session";
  source: string;                // adapter id
  app?: string; repo?: string;   // dimension keys
  detail?: Record<string, unknown>;
}

Deterministic rollup

buildDailyRecord is pure (input events → record, no side effects, no model). Each app-focus event contributes one poll interval to its app’s time; a sleep gap never inflates time because launchd fires no polls while asleep. The classifier (classify.ts) splits apps into creation / consumption / neutral by a static, editable map — the honest v1 proxy for challenge C1. narrative and telosTags are reserved seams, null/empty in v1.

Integration with TELOS (current → ideal)

  • Join key = TELOS IDs. The v2 scoring layer tags each block with a goal/dimension/ challenge ID; that tag is the foreign key between observed and ideal.
  • Read side: the daily rollup reads live TELOS as its rubric — no cached goal copy, so a TELOS change is picked up on the next rollup (no drift).
  • Write side: Conduit writes the daily record; UpdateLifeosState.ts gains an observed-time signal alongside the self-reported one.
  • Invariant: TELOS owns ideal state, Conduit owns observed state; neither writes the other’s file. They meet only at the gap computation (LIFEOS_STATE/ComputeGap). Conduit may never edit a goal.
  • Update mechanism: reuse DerivedSync.ts + launchd — the daily-record write triggers the LIFEOS_STATE recompute. Three clocks: continuous capture, daily rollup, event-driven derived refresh.

CLI

bun LIFEOS/PULSE/Conduit/conduit.ts <command>
  capture         run enabled adapters once, append events, lazy-roll the prior day
  rollup [date]   build + persist the daily record (default: today)
  today           print today's live distribution (not persisted)
  status          config + today's event count
  init            write default config under USER
  version         print Conduit version

Install (launchd)

bun LIFEOS/PULSE/Conduit/InstallConduit.ts             # install + load (polls every 120s)
bun LIFEOS/PULSE/Conduit/InstallConduit.ts --status    # launchd state
bun LIFEOS/PULSE/Conduit/InstallConduit.ts --uninstall # unload + remove

Pulse surfaces (data/code separated)

  1. Dashboard tab (/conduit) — the primary surface, a Next.js client component at Observability/src/app/conduit/page.tsx (static-exported to out/, served by pulse.ts). It holds ZERO data: it fetches everything live from /api/conduit/* and renders. Three panels answer the three questions the sensory layer exists to answer — what’s flowing in (the hourly content-type read), sources & cadence (per-source status), and the deterministic time/creation view — plus an honest empty-state when no read exists yet.
  2. JSON API (PULSE/modules/conduit.ts, loaded in pulse.ts loadModules(); read-only over USER/CONDUIT):
    • GET /api/conduit/today — today’s live deterministic record
    • GET /api/conduit/recent?days=N — last N daily records
    • GET /api/conduit/sourcesper-source status: id, label, one-line “what it captures”, enabled, pollIntervalSec, eventsToday, lastEventTs. Deterministic — from config + events, no model.
    • GET /api/conduit/insight — the latest content-type read (falls back to the most recent day when today’s is absent, stale: true; a failed/empty read serves available: false).
    • GET /api/conduit/status — module health.

All hold zero data in code — they render/serve the contents of USER data.

Content-type read (the hourly cheap-inference layer)

The “what kind of stuff is coming in?” question is the one genuinely non-deterministic part, so it — and only it — uses a model. Conduit/BuildInsight.ts runs hourly under launchd (com.lifeos.conduit.insight, InstallConduitInsight.ts), reads the day’s events, builds a metadata-only, bounded summary (top-15 apps + counts, git subjects, session slugs — never content), and calls LIFEOS/TOOLS/Inference.ts --level low (haiku rung, subscription billing — Inference scrubs ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN). It writes USER/CONDUIT/insights/<date>.json (narrative + weighted contentTypes[]). Cheap & sustainable by construction: cheapest rung, one call per hour, idempotent — a since watermark skips the call entirely on an idle hour, and a failed read never clobbers a good one. The tab reads the cached file; it never calls a model on load.

launchd gives jobs a minimal PATH that lacks claude, so the installer bakes the installer’s PATH into the plist (EnvironmentVariables) — otherwise the scheduled job spawn ENOENTs and falls back forever. This is macOS-only; a Linux install needs a cron equivalent for the hourly schedule.

Configuration (USER/CONDUIT/config.json)

{
  "enabled": true,
  "pollIntervalSec": 120,
  "sources": { "appFocus": true, "git": true, "claudeSession": true },
  "repos": [],
  "retentionDays": 30
}

Per-source opt-in is first-class. repos is the list of absolute paths watched for commits. Raw event logs older than retentionDays are pruned after rollup; daily records are kept.

Privacy contract

  • Local-only; all data under USER/CONDUIT/; nothing leaves the machine.
  • Per-source opt-in; disable any source in config.
  • Tiered retention: raw discarded after retentionDays; daily record kept.
  • Kill switch: enabled: false, or InstallConduit.ts --uninstall.
  • No screenshots, no window titles, no message content in v1.

Versioning

CONDUIT_VERSION (version.ts) tracks the component; bump on any change to the event schema, adapter contract, or rollup format. Conduit v1.0.0 ships in LifeOS 6.1.0.

Roadmap

  • v1 (shipped): deterministic capture (appFocus + git + claudeSession) → daily record; launchd; Pulse module; retention.
  • v1.1 (shipped 2026-07-05): intuitive tab — sources & cadence panel (/api/conduit/sources), the hourly cheap-inference content-type read (BuildInsight.tsinsights/<date>.json/api/conduit/insight), honest empty/stale states. Subscription inference, --level low, idempotent skip-on-idle. This is the pragmatic realization of the narrative seam via subscription inference (vs the Ollama-class local model originally sketched for v2).
  • v2 — TELOS scoring: activity→TELOS-goal tagging, filling the telosTags seam. Gated by the D-11 validation spike (can a cheap model reliably tag a synthetic day against TELOS?).
  • v3 — reach adapters: wire _INBOX / _CALENDAR as tier-1 account readers.
  • v1.5 — browser adapter: Dia/Chrome extension → active URL (makes in-browser C1 visible).
  • later: iMessage metadata; optional screenshot/vision fallback.

Cross-references

  • Design ISA: LIFEOS/MEMORY/WORK/20260704-conduit-context-gathering/ISA.md
  • Pulse: LIFEOS/DOCUMENTATION/Pulse/PulseSystem.md
  • Memory: LIFEOS/DOCUMENTATION/Memory/MemorySystem.md
  • TELOS derived-state: LIFEOS/TOOLS/UpdateLifeosState.ts, DerivedSync.ts