Gap-Closer
Gap-Closer is the active homeostasis system for what silicon-zack does not know about bio-zack. It turns unknown domains into ranked questions and reopens known-but-perishable beliefs when they age past their useful life. The goal is not a flattering βcoverage percentageβ; it is an honest, actionable gap map.
Status: active. VitaScheduler runs the overnight pass at 1:15am PT after
/sleep. The reactive surface answers βI have N minutesβwhat should we talk about?β from a dedicated Telegram topic.
The two engines
life ontology ββ ontology diff βββββββββββββΊ fill gaps
β² unknown β ask β durable knowledge
β
profile + identity + observations
β
ββ perishability decay ββΊ verify gaps
old belief β re-check β fresh belief
both ββΊ rank by value/minute ββΊ audit + optional nudge
Ontology diff: make unknown-unknowns legible
data/gapcloser/ontology.json describes broad life facets across four axes:
past, present, future, and stable. Each node declares its domain, facet,
label, likely probe, obviousness, perishability, expected minutes, profile
sources, keywords, and optional identity domain.
The diff measures each node against consolidated profile text, curated
profile/identity.json domain coverage, and matching identity observations. A
node below the configured fill threshold (default 0.25) becomes a fill gap.
Priority is intentionally simple:
fill priority = obviousness Γ (1 β coverage)
That makes an obvious but completely blank domain outrank a subtle domain that already has meaningful evidence.
Perishability decay: verify without waiting for a contradiction
Known facts are not all equally durable. The ontology assigns a perishability
class; each class has a half-life and weight. A nonblank live, high, or
medium belief whose last verification is older than its half-life becomes a
verify gap even when no new event says it changed.
verify priority = perishability weight Γ min(age / half-life, 2)
This catches quiet drift: a current plan, preference, relationship state, or project can expire without generating a crisp change signal.
Honest coverage
Coverage is deliberately conservative:
- The base is the stronger of consolidated profile evidence and the curated identity-domain coverage.
- A large shared profile file counts weakly when it does not actually mention the node's topic.
- Observations add a small liftβ0.045 each, capped at 0.18βand cannot by themselves make a blank domain look well known.
- Single keywords use word-boundary matching; phrases use substring matching.
- Zero evidence remains exactly
0.0, meaning unknown.
This is a gauge, not a target to maximize. The useful evaluation is whether the audit names important missing or stale beliefs, not whether a global percentage goes up.
Gap model and lifecycle
An ontology-backed gap is keyed by node_id; a conversational thread can have no
node. The durable record carries its probe, belief, evidence pointers, coverage,
priority, value per minute, probe/landing counts, and verification timestamps.
fill: open β probed β filled
β
ββ if evidence regresses below threshold β open
verify: stale/open β probed β verified
β
ββ after half-life β stale
either path β dismissed (never resurface)
Open rankings include open, probed, and stale records. The persisted
value_per_minute is priority / max(est_minutes, 1).
Overnight pass
At 1:15am PT, after /sleep has refreshed observations and identity state, the
overnight runner:
- loads the ontology, gap map, state, profiles, and observations;
- runs both diff engines;
- rewrites the current gap map atomically;
- snapshots per-node coverage and the run timestamp;
- writes
data/gapcloser/audit.md; and - optionally queues one prompt in the Gap-Closer Telegram topic.
The audit reports known/thin/blank counts, domain coverage weakest-first, the highest-value blanks, and overdue perishable beliefs. It is the direct answer to βwhat don't you know about me?β
The nudge is self-gated. It sends at most once per local day, only when enabled, only when a gap exists, and only when the top gap clears the configured value-per-minute threshold. It asks for a time budget rather than dumping all questions. Queue admission is checked; rejection does not falsely record a sent nudge.
Reactive session
select_probes(minutes, max_items=4, mode=None) greedily packs the ranked gaps
into the available time. It always selects at least the top gap, even if that
gap's estimate exceeds the budget, then marks chosen gaps probed and logs the
serve outcome so the next request rotates toward fresh ground.
After the conversation, the order of operations matters:
- write the answer into durable observations/profile/identity state;
- call
ingest(..., landed=True, belief=<current belief>)to close the gap.
Successful ingest marks a fill gap filled or a verify gap verified, refreshes
last_verified, increases coverage to at least 0.6, records the node's
verification time, and logs the outcome. landed=False reopens it as a fizzled
probe. dismiss permanently removes a gap from resurfacing. thread captures a
new conversation-derived gap that was not present in the ontology.
CLI
uv run python -m scripts.gapcloser.cli overnight [--no-nudge]
uv run python -m scripts.gapcloser.cli serve --minutes 15 [--mode fill|verify]
uv run python -m scripts.gapcloser.cli audit
uv run python -m scripts.gapcloser.cli status
uv run python -m scripts.gapcloser.cli ingest <gap-or-node-id> [--belief "..."]
uv run python -m scripts.gapcloser.cli ingest <gap-or-node-id> --fizzled
uv run python -m scripts.gapcloser.cli thread "label" "probe" --minutes 10
uv run python -m scripts.gapcloser.cli dismiss <gap-or-node-id>
overnight --no-nudge is the safe diagnostic pass. It still refreshes the map,
audit, and state, but does not enqueue Telegram output.
Storage
| Path | Purpose | Write pattern |
|---|---|---|
data/gapcloser/ontology.json |
Life-domain ontology and decay configuration | Curated JSON |
data/gapcloser/gaps.jsonl |
Current gap records | Full atomic rewrite |
data/gapcloser/probe_log.jsonl |
Served, landed, fizzled, dismissed outcomes | Atomic append |
data/gapcloser/state.json |
Run time, config, coverage snapshot, last verification | Atomic write |
data/gapcloser/audit.md |
Current human-readable gap audit | Atomic write |
Despite the .jsonl name, gaps.jsonl is a current-state snapshot rewritten in
full, not an append-only history. The probe log is the append-only learning
record.
Reliability and limitations
- The scheduler uses the shared task policy and runs the synchronous pass off the
event loop with
asyncio.to_thread. - Malformed gap JSONL records and observations are skipped, not fatal.
- The ontology is curated and necessarily incomplete. Conversational
threadgaps are the escape hatch, but ontology growth is still manual. - Shared profile files can still overstate depth even with topical dampening; coverage should never be treated as a truth score.
- Selection is greedy by value per minute, not an optimal knapsack or a model of conversational coherence.
ingest(landed=True)without a newbeliefis allowed but logs a warning: it can refresh a timestamp while preserving stale wording, producing a stopped-clock βverifiedβ belief. Callers should pass the durable belief.- Probe outcomes are logged for self-tuning, but the ranking constants are not yet automatically learned from those outcomes.
File map
| Path | Responsibility |
|---|---|
scripts/gapcloser/models.py |
Gap schema, current-state store, config, probe log |
scripts/gapcloser/engine.py |
Coverage, ontology diff, decay, ranking |
scripts/gapcloser/overnight.py |
Nightly refresh, audit, nudge |
scripts/gapcloser/serve.py |
Budgeted selection, ingest, thread, dismiss |
scripts/gapcloser/cli.py |
Operator CLI |
api/src/scheduler.py |
1:15am PT loop and manual runner |
api/src/loop_engine/registry.py |
Canonical scheduler registration |
Related documentation
- Identity & Interviews β the durable knowledge Gap-Closer measures
- Homeostasis & Predictions β the wider calibration model
- Telegram β the conversation surface and topic routing
- Proactive Outreach β queue admission and delivery behavior
- Scheduling Engine β task policies and liveness