# Core ↔ Agent Bridge — v1.11.0

The OpenLife CLI ships in two profiles that, since v1.11.0, **know about each
other** and share memory:

| Profile           | Role                                                  |
| ----------------- | ----------------------------------------------------- |
| `openlife-core`   | Short-lived interactive CLI                           |
| `openlife-agent`  | Long-running daemon (`openlife start --daemon`)       |

Before v1.11, each cwd's `.openlife/` was isolated, neither side could
discover the other, and memory writes were non-atomic file appends scoped to
the cwd. v1.11 fixes all three.

---

## 1. Mutual discovery — `gateway-manifest.json`

When the daemon starts, it publishes a manifest:

```
~/.openlife/gateway-manifest.json
```

(or `<OPENLIFE_STATE_DIR>/gateway-manifest.json` when that env var is set —
project-scoped daemons).

```json
{
  "schemaVersion": 1,
  "host": "127.0.0.1",
  "port": 3000,
  "token": "<32-byte hex>",
  "pid": 12345,
  "profile": "autonomous",
  "startedAt": "2026-05-20T17:50:00.000Z",
  "refreshedAt": "2026-05-20T17:50:30.000Z",
  "telegram": { "enabled": true, "allowedUserId": "1344110010" },
  "version": "1.11.0"
}
```

- File mode is `0o600`. A sidecar `gateway-token` file contains just the
  token for shell scripts that don't want to parse JSON.
- `refreshedAt` is updated every 30s (same cadence as the watchdog
  heartbeat). Staleness mirrors `openlife status` thresholds:
  - `fresh`: `age <= 60s`
  - `stale`: `60s < age <= 300s`
  - `dead`:  `age > 300s`
- The token is the Basic Auth password for `/api/v1/*` (user is the fixed
  literal `openlife-core`). It rotates on every daemon start.
- On graceful shutdown the manifest is deleted, so `AgentDiscovery`
  reports `liveness: 'missing'` immediately instead of waiting 5 minutes
  for the staleness clock to expire.

---

## 2. CLI: `openlife bridge ...`

From any cwd:

```bash
openlife bridge status
openlife bridge push <verb> [args...] [--text "<chat text>"] [--no-sync]
openlife bridge pull
openlife bridge listen [--since=<seq>]
openlife bridge memory <query...>
```

### `bridge status`
Reads the manifest via the multi-root resolver (cwd-local first, then
home-scoped) and prints `{ alive, liveness, ageSeconds, manifest }`. Exits 0
if alive, 1 otherwise.

### `bridge push <verb>`
POSTs `/api/v1/core/run` on the discovered daemon. Built-in verbs:
- `chat` — routes through `processChat(userId, text)`. Requires `--text`.
- `status` — daemon-side state snapshot.
- `memory` — `OmniMemory.search(text)`.

Custom verbs return `unknown_verb` until extended.

### `bridge pull`
Drains pending messages from `~/.openlife/cli-inbox/`. TTL-expired files
are deleted automatically.

### `bridge listen`
Tails `/api/v1/events` as Server-Sent Events. Resumable via
`--since=<seq>` or the standard `Last-Event-Id` header. Events emitted:
- `command` — every POST `/api/v1/core/run`
- `pipeline` — chat classify/route/finalize stages
- `memory` — OmniMemory lookups via the bridge
- `telegram` — `/core ...` family
- `mission` / `governance` — reserved for future wiring

### `bridge memory <query>`
Searches `OmniMemory` locally — works even if the daemon is offline.

---

## 3. Eternal persistent memory

`LocalMemoryProvider` now:

1. Stores records at `<OPENLIFE_MEMORY_DIR or ~/.openlife/memory>/memory-records.json` — shared across cwds, shared between core and daemon.
2. Writes via `AtomicWriter` (temp + fsync + rename) — a `kill -9` mid-write never produces half-written JSON.
3. Serializes concurrent writers through a `DistributedLock` (5s TTL,
   auto-takeover when the holder crashes).
4. Migrates `<cwd>/.artifacts/memory-records.json` → new location on first
   boot. The legacy file is **never deleted** (user-data safety).

### Pluggable cloud backends

Set `OPENLIFE_MEMORY_PROVIDER` to switch:

| Value           | Backend                          | Env vars required               |
| --------------- | -------------------------------- | ------------------------------- |
| `local`         | File-backed (default)            | —                               |
| `mem0`          | Mem0 cloud                       | `MEM0_API_KEY`                  |
| `honcho`        | Honcho.dev REST API              | `HONCHO_API_KEY`, `HONCHO_APP_ID` |
| `supermemory`   | Supermemory.ai REST API          | `SUPERMEMORY_API_KEY`           |
| `redis-ams`     | Redis-backed agent memory store  | `REDIS_URL` (provider-defined)  |
| `zep-graphiti`  | Zep Graphiti                     | provider-defined                |

When the primary is not `local`, `MemoryOrchestrator` ALSO writes through to
the local file cache. Reads use the primary with local fallback on transient
errors. Consequence: switching providers never loses your local facts, and
reads survive a cloud outage.

### Eternal records

```ts
omni.saveFact('Rafael prefers vibe-coding', meta, namespace, { keep: true });
```

`keep: true` exempts the record from retention expiry regardless of `type`.

---

## 4. Reverse IPC — daemon → core via file queue

The daemon can leave notes for the local CLI even when the CLI isn't
running. Layout:

```
~/.openlife/cli-inbox/
├── <ulid>.json        # CoreInboxMessage
└── replies/
    └── <ulid>.json    # written by core after handling
```

Each `openlife <command>` invocation:

1. Writes `~/.openlife/core-presence.json` (advisory; TTL 60s) so the
   daemon knows which cwd you most recently used.
2. Peeks the inbox and, if anything is pending, prints a one-line banner
   to stderr. The actual processing happens via explicit
   `openlife bridge pull` — nothing auto-executes (security).

### Telegram `/core ...` family

When the daemon is running, send these to your Telegram bot:

| Command | Routed via | Behaviour |
| ------- | ---------- | --------- |
| `/core status`            | direct | Daemon prints `core-presence.json` summary |
| `/core memory <query>`    | direct | Daemon runs `OmniMemory.search` immediately |
| `/core agents [<id>]`     | direct | Reads `.catalog/agents/` from daemon's cwd |
| `/core run <task...>`     | inbox  | Enqueued for the core CLI to handle via `bridge pull` |
| `/core flow run <id>`     | inbox  | Enqueued (cwd-sensitive) |

Single-user allowlist (`OPENLIFE_TELEGRAM_ALLOWED_USER_ID`) gates all of
the above.

---

## 5. Security model

- **Authentication.** All `/api/v1/*` endpoints require Basic Auth with the
  manifest token. Anyone who can read `~/.openlife/gateway-manifest.json`
  already has filesystem access to the home directory — same trust
  boundary as `.env`. Manifest mode is `0o600`.
- **Telegram.** Single-user allowlist via
  `OPENLIFE_TELEGRAM_ALLOWED_USER_ID` (default `1344110010`). The
  `/core` family inherits this.
- **Reverse IPC.** `~/.openlife/cli-inbox/` is per-user. Inbox files mode
  `0o600`. Messages older than TTL deleted on drain.
- **Cloud memory tokens.** `MEM0_API_KEY`, `HONCHO_API_KEY`,
  `SUPERMEMORY_API_KEY` are read from env/`.env` only — never logged,
  never written to the manifest.
- **No new network surface.** Express server still listens on 3000 (or
  `PORT`); SSE rides the same port. File-queue is local-only.

---

## 6. Quick smoke

Terminal A:
```bash
openlife start --daemon
# wait for "[GATEWAY] gateway-manifest publicado para discovery do CLI core"
```

Terminal B (different cwd):
```bash
openlife bridge status
# → { alive: true, liveness: 'fresh', port: 3000, ... }

openlife bridge push status
# → daemon's own status JSON

openlife bridge memory "rafael preferences"
# → list of matching facts from local file

openlife bridge listen
# → SSE stream prints events as they happen
```

Telegram:
```
/core status           # last cwd + freshness
/core memory rafael    # OmniMemory.search via daemon
/core run npm test     # enqueues into cli-inbox
```

Switch memory backend:
```bash
export OPENLIFE_MEMORY_PROVIDER=honcho
export HONCHO_API_KEY=xxx
export HONCHO_APP_ID=yyy
openlife bridge memory "anything"   # primary=honcho, write-through to local
```
