Local-first Personal Model Runtime for macOS
Persome is one local daemon with one production ingestion path. It observes a person, forms bounded state, updates an auditable personal model, and exposes that model to local consumers. There is no product workflow or predictor hidden beside this path.
flowchart LR
AX["macOS AX watcher"] --> S0["S0 debounce and dedup"]
ING["trusted local ingest"] --> S1
S0 --> S1["S1 focused element, visible text, URL"]
OCR["optional local OCR worker"] --> CAP
S1 --> CAP[("capture buffer and captures FTS")]
CAP --> TL["one-minute timeline normalizer"]
TL --> BL[("timeline_blocks")]
BL --> SES["three-rule session cutter"]
SES --> RED["five-minute active reducer"]
RED --> EVT[("event daily memory")]
RED --> ACTIVE["active-window modeling"]
ACTIVE --> DELTA["evidence-gated memory_delta"]
SES -->|session end| FIN["trailing-window finalizer"]
EVT --> FIN
BL --> FIN
FIN --> DELTA
DELTA --> APPLY["deterministic Point and Line apply"]
FIN --> PAT["repeated behavior memory"]
APPLY --> EVO[("evomem, Markdown, FTS")]
PAT --> EVO
EVO --> BUILD["case, schema, cross-domain, Root, layout"]
BUILD --> SNAP["versioned personal model"]
SNAP --> MCP["MCP"]
SNAP --> CHAT["Chat"]
SNAP --> VIEW["localhost /model"]
capture/event_dispatcher.py performs
event deduplication, debounce, and minimum-gap control.capture/scheduler.py builds an S1 record. AX is primary. When explicitly
enabled and AX text is poor, a focused screenshot is sent to an isolated
local OCR subprocess; its text is backfilled into captures FTS.timeline/aggregator.py consults both the capture JSON and OCR backfill,
removes UI repetition, preserves authored evidence, and writes wall-clock
aligned one-minute blocks.session/manager.py cuts work using idle-gap, single-app soft-cut, and
maximum-duration rules.writer/session_reducer.py flushes active sessions every five minutes;
writer.agent.model_active_session turns each new window into Points/Lines.
Session end writes and models only the trailing range.Every successful active flush enters writer.agent.model_active_session and
the windowed memory-delta path. Every reduced session then enters
writer.agent.finalize_session, regardless of
whether the terminal reducer wrote a new entry. This matters when prior flushes
already covered the whole session or when the reducer exhausted its LLM retries
and wrote a heuristic fallback.
The finalizer runs:
skills/skill-*.md;memory_delta extraction over the unmodeled tail;Each memory-delta window is persisted before apply. apply_status allows a
crashed apply to resume without another LLM call or duplicate relation
reinforcement. delta_end advances after successful active apply; the session
receives modeled_at only after all terminal stages finish. A kernel
session-model.lock coordinates daemon, retry, CLI, and model-build callers.
New Point/Line evidence triggers a debounced build every 30 minutes by default;
persome model build and the unconditional 00:15 build call the same locked
coordinator:
/model.Each stage records complete, skipped, or failed. Missing geometry or a failed
enabled substage makes the build degraded. The build never fabricates an
empty replacement for a previously valid Root.
The registry in src/persome/daemon.py is the authoritative task list.
| Task | Cadence and responsibility |
|---|---|
capture |
Continuous AX watcher or trusted ingest runner; writes deduplicated S1 captures and updates session activity. |
session |
Every session.tick_seconds; evaluates idle, soft-cut, and timeout boundaries. |
reducer-retry |
Every 60 seconds; consumes next_retry_at, then sends reduced or heuristic terminal results through the shared finalizer. |
daily-safety-net |
At 23:55 by default; force-ends the open session, catches all stranded reduction/modeling work, reprojects, checkpoints, snapshots, prunes telemetry, and runs enabled maintenance. |
timeline |
Every 60 seconds; materializes closed timeline windows and applies capture retention. |
flush |
Every session.flush_minutes; reduces and models the new active-session window as Points/Lines. |
classifier-tick |
Legacy-only: every classifier.interval_minutes when delta apply is disabled. |
vector-embed-tick |
Every 60 seconds when hybrid retrieval is enabled; drains the embedding queue. It is a no-op without credentials. |
model-refresh |
Every schema.refresh_minutes when new Point/Line evidence exists; refreshes Face/Volume/Root. |
schema-tick |
At 00:15 by default; invokes the shared personal-model build. |
mcp |
Hosts streamable HTTP MCP, REST, Chat routes, and /model; restarts with backoff after a crash. |
--capture-only keeps capture, session, reducer-retry, the daily safety
net, and configured MCP. It disables timeline, flush, classifier, vectors, and
schema/model processing. It is a diagnostic/embedding mode, not a second
ingestion architecture.
src/persome/paths.py owns every location. The default root is ~/.persome.
| Artifact | Role |
|---|---|
capture-buffer/*.json |
Bounded raw S1 records and optional encrypted screenshots. |
memory/*.md |
Human-readable event, fact, schema, and correction history. |
memory/skills/skill-*.md |
Evidence-backed repeated behavior. |
index.db |
WAL-mode sessions, FTS5, evomem, relations, geometry, receipts, vectors, and audit tables. |
model-build.json |
Owner-only build conditions and stage outcomes. |
exports/*.json |
Owner-only, redacted-by-default snapshots. |
backup/*.db |
Verified daily SQLite snapshots when enabled. |
Markdown is the default write authority and evomem is its maintained shadow.
An operator may explicitly invert authority to evomem; this does not change the
public snapshot contract. SQLite access must use with fts.cursor() as conn: so
readers and writers coexist under WAL mode.
persome chat or the loopback Chat REST routes; it uses the same
memory and model, not a second store.http://127.0.0.1:8742/model while the daemon HTTP server is
active. It reads /model/graph and packaged local Three.js assets.The Runtime contains no click/type actuation, notification lifecycle, meeting audio, or evaluation runner.
delta_end does not advance, so the next flush retries a larger window.modeled_at remains null and retry/recovery can resume.model-build.lock waits or reports busy.See capture.md, timeline.md, session.md, and writer.md for stage details.