Skip to content

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:

  1. The base is the stronger of consolidated profile evidence and the curated identity-domain coverage.
  2. A large shared profile file counts weakly when it does not actually mention the node's topic.
  3. Observations add a small liftβ€”0.045 each, capped at 0.18β€”and cannot by themselves make a blank domain look well known.
  4. Single keywords use word-boundary matching; phrases use substring matching.
  5. 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:

  1. loads the ontology, gap map, state, profiles, and observations;
  2. runs both diff engines;
  3. rewrites the current gap map atomically;
  4. snapshots per-node coverage and the run timestamp;
  5. writes data/gapcloser/audit.md; and
  6. 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:

  1. write the answer into durable observations/profile/identity state;
  2. 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 thread gaps 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 new belief is 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