# OpenLife v1.11 — Changelog

**Branch:** `main`
**Predecessor:** v1.10.0 (tagged 2026-05-18)
**Released:** 2026-05-20

v1.11 is the **Core ↔ Agent Bridge** milestone. It closes three gaps
that existed since the dual-profile split (v1.8):

1. **Mutual discovery.** Before v1.11, the openlife-core CLI and the
   openlife-agent daemon had no way to know about each other. Each
   cwd's `.openlife/` was isolated.
2. **Eternal persistent memory.** `LocalMemoryProvider` wrote
   non-atomically to `<cwd>/.artifacts/`, scoped per cwd, with no
   concurrency guarantee.
3. **Bidirectional commanding.** Core could call daemon via HTTP only
   if it already knew the port + token. Daemon had no reverse path to
   instruct the core at all.

The v1.11.0 release ships all three closed plus two new cloud memory
backends (Honcho, Supermemory) and a complete `/core ...` Telegram
verb family.

## What landed by subsystem

### Subsystem 1 — Mutual discovery (Phase 1)

| Component | Surface |
|---|---|
| `src/orchestrator/util/GatewayManifest.ts` | `publish(opts) / refresh() / remove() / peek()`. Writes `~/.openlife/gateway-manifest.json` (or `<OPENLIFE_STATE_DIR>/gateway-manifest.json` when set). File mode `0o600`. 32-byte hex token (sidecar `gateway-token` for shell scripts). |
| `src/cli/AgentDiscovery.ts` | `discover()` → `{ alive, liveness, manifest, ageSeconds, source, path }`. cwd-local manifest takes precedence over home-scoped (multi-root pattern v1.8.3). Staleness thresholds mirror StatusCommand: `fresh<=60s`, `stale<=300s`, `dead>300s`. |
| `src/orchestrator/Gateway.ts` | `Gateway.start()` publishes the manifest inside the listen callback (reads actual bound port via `server.address()`). Watchdog tick refreshes `refreshedAt`. `Gateway.shutdown()` removes the manifest so AgentDiscovery sees `missing` immediately instead of waiting 5min for staleness. |
| `src/orchestrator/util/WatchdogHeartbeat.ts` | New `onTick` callback hook so manifest refresh piggybacks on the existing 30s cadence — no second timer. |

### Subsystem 2 — Eternal memory (Phase 2)

| Component | Surface |
|---|---|
| `src/memory/LocalMemoryProvider.ts` | Replaced non-atomic `fs.writeFileSync` with `AtomicWriter.writeJsonAtomic`. Concurrent writers serialize through `DistributedLock` (5s TTL, auto-takeover on holder death, bounded retry). Default store path is `OPENLIFE_MEMORY_DIR ?? ~/.openlife/memory/`. One-time migration from legacy `<cwd>/.artifacts/memory-records.json` (legacy file kept intact for user-data safety). |
| `src/memory/MemoryRetentionPolicy.ts` | New `keep: true` flag bypasses type-based expiry. Eternal records survive regardless of `MemoryType`. |
| `src/memory/MemoryProvider.ts` | `MemoryRecord` gains optional `keep?: boolean`. |
| `src/memory/HonchoMemoryProvider.ts` | New cloud provider. Env: `HONCHO_API_KEY`, `HONCHO_APP_ID`. Graceful degradation on missing config or network errors — never throws. |
| `src/memory/SupermemoryMemoryProvider.ts` | New cloud provider. Env: `SUPERMEMORY_API_KEY`. Same graceful contract. |
| `src/memory/MemoryProviderRegistry.ts` | New `selectPrimary()` reads `OPENLIFE_MEMORY_PROVIDER` (case-insensitive). Valid: `local`, `mem0`, `honcho`, `supermemory`, `redis-ams`, `mempalace`, `zep-graphiti`. Unknown values fall back to `local` with a warning. |
| `src/memory/MemoryOrchestrator.ts` | When primary != local, ALSO writes through to local cache. Reads use primary with local fallback on transient errors. Per-provider error handling — one failing provider never halts the search. |
| `src/memory/OmniMemory.ts` | Removed hardcoded `providers: ['local']` from save/search calls. `saveFact()` accepts optional `{keep: true}` for eternal records. |

### Subsystem 3 — Bridge HTTP + CLI (Phase 3)

| Component | Surface |
|---|---|
| `src/cli/AgentBridgeCommand.ts` | New Commander subcommand group under `bridge` namespace (preserves legacy `openlife agent <verb>` aliases): `bridge status / push <verb> [args] / pull / listen / memory <query>`. Lazy-loaded via `require` in `src/index.ts` so `--help` stays under 500ms. |
| `src/orchestrator/Gateway.ts` | New endpoints `POST /api/v1/core/run` (verbs: `chat`, `status`, `memory`) and `GET /api/v1/events` (Server-Sent Events with 1000-event bounded ring buffer, `Last-Event-Id` resumption). Basic Auth via manifest token (user fixed to `openlife-core`). |
| Event bus | Internal `emitBridgeEvent(kind, payload)` publishes to all SSE subscribers + appends to ring buffer. Kinds: `command`, `pipeline`, `memory`, `telegram`, `mission`, `governance`. |

### Subsystem 4 — Reverse IPC (Phase 4)

| Component | Surface |
|---|---|
| `src/cli/CliInbox.ts` | ULID-id file queue at `~/.openlife/cli-inbox/`. Operations: `enqueue / drain / acknowledge / reply / readReply / waitForReply`. TTL 24h default, expired messages deleted on drain. Reply round-trip via `<inbox>/replies/<id>.json`. |
| `src/cli/CorePresence.ts` | Publishes `~/.openlife/core-presence.json` on every CLI invocation. Records: `{ pid, cwd, invokedAt, command, argv }`. TTL 60s. Mode `0o600`. |
| `src/orchestrator/CoreRemoteCommand.ts` | Telegram `/core <verb>` router. Direct verbs (answered by daemon, zero round-trip): `status`, `memory`, `agents`. Inbox verbs (enqueued for core): `run`, `flow`. |
| `src/orchestrator/Gateway.ts` | `processInput` detects `/core ` prefix BEFORE classifier and routes to `CoreRemoteCommand.handle()`. Inherits the existing `OPENLIFE_TELEGRAM_ALLOWED_USER_ID` allowlist. |
| `src/index.ts` preAction hook | On every CLI invocation: `CorePresence.publish(actionCommand.name())` + non-blocking `CliInbox.drain()` with one-line banner when pending messages exist (silent when `OPENLIFE_INBOX_SILENT=true`). |

### Subsystem 5 — Tests + ship (Phase 5)

| Test file | Coverage |
|---|---|
| `src/test_gateway_manifest.ts` | publish round-trip, refresh preserves token + advances refreshedAt, remove cleans both files (idempotent), refresh-before-publish is no-op. Verifies file mode 0o600 on POSIX. |
| `src/test_agent_discovery.ts` | cwd-local takes precedence over home, staleness classification fresh→stale→dead, missing manifest, malformed manifest skipped. |
| `src/test_cli_inbox.ts` | enqueue→drain round-trip, reply round-trip via waitForReply, TTL expiry deletes old files, acknowledge is idempotent. |
| `src/test_local_memory_atomic.ts` | `OPENLIFE_MEMORY_DIR` honored, 50 parallel saves produce parseable JSON with no temp residue, legacy migration preserves old file. |
| `src/test_memory_provider_selection.ts` | unset env → local, known providers returned as primary, unknown falls back, `keep:true` bypasses retention, primary != local writes through to local cache. |
| `src/test_core_remote_command.ts` | parse extracts verb + args, `/core status` answered directly via CorePresence, `/core run` enqueues into cli-inbox, unsupported verb rejected. |
| `src/test_agent_bridge_e2e.ts` | End-to-end smoke: mini daemon + manifest publish + AgentDiscovery + 200/403/401 auth round-trip. |

All 7 new tests pass. All 4 adjacent suites (watchdog, atomic-writer, memory-orchestrator, memory-retention) verified no regression.

## Verification beyond unit tests

Manual smoke against the actual Gateway (not mocks):

- 13 inline assertions in `smoke-bridge.js`: manifest publication, port binding, token validation, 200/403 auth, SSE event delivery, watchdog refresh advance, token stability across refresh, shutdown cleanup.
- Cross-cwd CLI smoke: `bridge status` from `/tmp` discovers daemon in `OPENLIFE_STATE_DIR=/tmp/sm-XXX`, `bridge push status` round-trips HTTP with manifest token, `bridge memory` works locally without daemon.

Result: full bridge stack working end-to-end.

## Security model

- **Authentication:** all `/api/v1/*` endpoints require Basic Auth with the manifest token. The token IS the password — anyone with read access to `~/.openlife/gateway-manifest.json` already has filesystem access to `~/`, same trust boundary as `.env`. Mode `0o600` enforced via `chmod`.
- **Telegram:** existing single-user allowlist via `OPENLIFE_TELEGRAM_ALLOWED_USER_ID` (default `1344110010`). `/core` family inherits it — only the configured user can trigger.
- **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` read from env/`.env` only — never logged, never written to manifest.
- **Token rotation:** every daemon start generates a fresh 32-byte hex token. Token does NOT rotate on watchdog refresh (stable for lifetime of daemon process).
- **No new network surface:** Express still listens on port 3000 (or `PORT`). SSE rides the existing port. File-queue is local-only.

## Out of scope (deferred to v1.12+)

- Cross-machine bridge (e.g., remote daemon via tailscale). Manifest is localhost-only in v1.11. Cloud sync via Redis/Mem0/Honcho/Supermemory remains the cross-machine path.
- mTLS / per-call token rotation.
- Multi-user mode for Telegram (still single-user by allowlist).
- Encrypted-at-rest memory store (filesystem permissions only today).
- Sharing the WorkflowEngine state across processes.

## Upgrade

```bash
# Already installed globally
openlife update --global

# Equivalent direct
npm install -g @openlife/cli@latest

# Fresh install
npm install -g @openlife/cli
openlife init
```

Verify:

```bash
openlife --version              # 1.11.0
openlife bridge --help          # lists status/push/pull/listen/memory
```

## File summary

- **New (10):** `src/orchestrator/util/GatewayManifest.ts`, `src/cli/AgentDiscovery.ts`, `src/cli/AgentBridgeCommand.ts`, `src/cli/CliInbox.ts`, `src/cli/CorePresence.ts`, `src/orchestrator/CoreRemoteCommand.ts`, `src/memory/HonchoMemoryProvider.ts`, `src/memory/SupermemoryMemoryProvider.ts`, `docs/core-agent-bridge.md`, `MANIFESTO.md`.
- **Modified (12):** `src/orchestrator/Gateway.ts`, `src/orchestrator/util/WatchdogHeartbeat.ts`, `src/memory/LocalMemoryProvider.ts`, `src/memory/MemoryProvider.ts`, `src/memory/MemoryRetentionPolicy.ts`, `src/memory/MemoryProviderRegistry.ts`, `src/memory/MemoryOrchestrator.ts`, `src/memory/OmniMemory.ts`, `src/index.ts`, `package.json`, `README.md`, `INSTALL.md`.
- **Tests (7):** see Subsystem 5 table.

## Commits

- `86da792` — feat(bridge): Core ↔ Agent Bridge + eternal memory (v1.11.0)
- `a4c071c` — chore(gitignore): ignore v1.11.0 bridge runtime state + evidence_log.md

## Release log

- 2026-05-20T19:25:11Z — tag `v1.11.0` pushed → workflow auto-triggered
- 2026-05-20T19:26:09Z — publish failed (NPM_TOKEN expired)
- 2026-05-20T19:51:02Z — NPM_TOKEN rotated in GitHub Secrets
- 2026-05-20T19:52:32Z — published to npm as `@openlife/cli@1.11.0`
