# Hearth ↔ crtr runtime compatibility — v4

A versioned contract for any external product (originally: `@crouton-kit/hearth`) that runs alongside a crtr install as a separate process/repo and needs a stable surface to pin a crtr package version against. "v4" names the shape described here; a future breaking change to any part of this contract gets a new compatibility document rather than silently rewriting this one.

## Status and migration from v3

v4 is a breaking hard cut of the scheduling surface. The entire `node trigger` command family (`node trigger send`, `node trigger revive`, and every other `--at`/`--every`/`--when`/`--until` trigger leaf) is removed with no alias — those calls now fail with `unknown subcommand` (exit 2). Every case `node trigger` covered — future delivery, a deferred revival, a deferred birth, or a predicate-gated wait — is now arranged through the general-purpose `crtr cron add` primitive: arm a cron on a clock schedule (`--at`/`--every`), or with a predicate gate written into the cron's own script, whose bash command does the work — for example `crtr node message send --to <id> ...` for future delivery, `crtr node lifecycle revive --fresh <id>` for a deferred revival, or `crtr node new` for a deferred birth (`crtr cron add -h`, `crtr cron -h`). `node wait deadline` is unchanged in shape — it already arms a self-anchored, cancel-on-wake cron under the hood rather than a trigger. The canvas `triggers` table is dropped; its live rows were converted into `crons`/`cron_runs` rows one-for-one at upgrade time, preserving each row's original id.

One behavior changed as a deliberate simplification, not a defect, and is worth knowing before you meet it in the wild: a scheduled message armed at `--tier deferred` against a target that later terminalizes before the message fires used to have the daemon quarantine the delivery and send the armer a single urgent notice. Under cron, the same case now fails the cron's run instead (the send rejects with `deferred_no_natural_cycle`), and a failing run's default disposition (`--on-output on-failure`) pauses the cron and spawns a fresh node to deal with it — fewer moving parts, at the cost of an escalation agent in place of a quiet notice. A consumer that wants the old quiet-drop behavior back should arm the cron with `--on-output silent` instead, which records the failed run in the run log and never escalates.

Sections 1–2, 4, and 5 are unchanged from v3; section 6 changes only its auxiliary-tables bullet (`triggers` → `crons`, `cron_runs`).

## 1. Import contract

See `docs/public-api.md` for the full picture. The subset this note assumes:
`general`, `nowIso`, and `BROKER_READ_CAPS`, all imported from the package
root (`@crouton-kit/crouter`). No subpath beyond `.`, `./cli`, `./web` exists
or is supported.

## 2. `crtr --json sys version`

Read-only. Returns an object with at minimum:

```json
{ "version": "0.3.40" }
```

`version` is a required string, a semver matching `package.json#version` of
the installed crtr. A consumer asserting a running crtr's version (e.g. after
a guest image roll) should exec this command in-guest and compare the
returned `version` against the expected target — do not parse `--help` output
or read `package.json` directly, since the installed layout is not a public
contract.

## 3. `crtr --json node message send` — immediate delivery

This leaf delivers an inbox message immediately. Future delivery, deadline waits (`node wait deadline`), and fresh revivals (`node lifecycle revive` for the immediate case) all have separate command paths and are outside this compatibility contract; deferred future delivery and deferred revival are arranged through `crtr cron add` rather than a dedicated leaf (see "Status and migration from v3" above). Typed-output requests (`node message request`) are likewise outside this contract.

**Invocation:**

```
<body on stdin> | crtr --json node message send --to <node-id> [--tier critical|urgent|normal|deferred]
```

- `body` — the message text, passed on stdin (or positional).
- `--to <node-id>` — required target (a real node id; `--self` is not
  applicable to an external caller).
- `--tier` — optional, one of `critical | urgent | normal | deferred`.
  Defaults to `normal` when omitted. Governs how the message is presented at
  the target's next turn, not delivery timing.

**Output** (JSON object, `outputKind: object`):

| field | type | when present |
|---|---|---|
| `target` | string | always — the target node id |
| `delivered` | boolean | always — `true` once the inbox entry is appended |
| `revived` | boolean | always — whether delivery revived a dormant target |
| `guidance` | string | always — human/agent-readable confirmation text |

**Delivery semantics — read carefully:** this appends an entry to the
target's `inbox.jsonl` and, if the target is dormant, makes a **best-effort**
attempt to revive it so the message is acted on. Revival is asynchronous —
the command returning `delivered: true` means the inbox write succeeded, not
that the target has processed the message or is now running. A caller must
not block synchronously waiting for a revive to complete as part of this
call; if a caller needs to know the target acted on the message, that is a
separate, out-of-band observation (e.g. polling node status or waiting for
the target to message back), not a guarantee `node message send` makes.

## 4. In-guest web port

`crtr surface web serve --host 127.0.0.1 --port <PRIVATE>` starts crtr's
unified web server (shell SPA, source/command bridge, SSE change lane, and
the browser⇄broker relay) bound to loopback on a private port chosen by the
guest. This is a **private, unauthenticated-by-default loopback surface** —
binding a non-loopback host requires `--token`; a consumer that fronts this
port (reverse-proxying non-model-auth or non-product traffic to it) is
responsible for whatever auth gate sits in front of the public listener. crtr
itself makes no claim about what proxies to this port or how; it only
guarantees the port serves the crtr web UI when bound.

## 5. Broker `view.sock` — the `reload_auth` subset

A minimal, dependency-free client can nudge a live broker to re-read
credentials after an external auth flow completes (e.g. a login page that
just wrote fresh OAuth tokens to disk) without importing crtr's
`ViewSocketClient` or any frame type. This is a v4-stable subset of the full
broker wire protocol (`src/core/runtime/broker-protocol.ts`).

### 5.1 Socket path resolution

- Always `<CRTR_HOME>/nodes/<nodeId>/view.sock` (`CRTR_HOME` defaults to
  `~/.crouter/canvas`). `CRTR_SOCK_DIR` is not read.

### 5.2 Framing

Newline-delimited JSON. Each frame is one JSON object serialized on a single
line, terminated by `\n`. Write frames to the socket in this form; read
replies the same way (buffer until a `\n`, then `JSON.parse` the line).

### 5.3 Handshake — `hello`

On connect, send:

```json
{"type":"hello","role":"observer","client_id":"<any string>"}
```

`role: "observer"` is sufficient for this subset — `reload_auth` is open to
any client (controller or observer), since it is an idempotent local
credential re-read that does not steer the conversation. The broker replies
with a `welcome` frame:

```json
{"type":"welcome","snapshot":{...},"role":"observer","controller_id":"<client-id-or-null>",...}
```

The `controller_id` is the current controller's client id (a real id if a controller is held, `null` if no controller is currently connected). Observer hello does not clear an existing controller.

**The `welcome.snapshot` can carry the full message/session history and may
be many megabytes** — the read side is capped generously (`CLIENT_READ_CAPS`:
256 MiB per frame and per total buffer), not tightly. A `reload_auth`-only
client must tolerate this frame arriving and either parse-and-discard it or
skip it structurally (it is still one JSON line — read and drop it) rather
than assuming the first frame is small.

### 5.4 The nudge — `reload_auth`

Send:

```json
{"type":"reload_auth"}
```

No other fields. The broker re-reads on-disk auth storage and refreshes its
model registry in response.

### 5.5 The reply — `ack`

```json
{"type":"ack","for":"reload_auth","ok":true}
```

- `for` echoes the frame type that was acted on.
- `ok: true` on success. On failure the broker may instead send an `error`
  frame (`{"type":"error","code":"...","message":"..."}`). A minimal client
  must read frames until it receives either `{"type":"ack","for":"reload_auth",...}` or `{"type":"error",...}` and IGNORE any non-terminal frames sent in between (notably: `model_changed` broadcasts after the ack). Only the explicit ack or error frame determines the result.

### 5.6 What this subset intentionally omits

No other frame type in this protocol (prompt/steer, session control, model
selection, read-op pickers, bash execution, etc.) is part of this v4 note. A
consumer that needs more than the `reload_auth` nudge should import
`ViewSocketClient` from the package root rather than hand-rolling a larger
client against this doc.

## 6. `canvas.db` byte-shape — version 1

**Read/compat constraint only — never a write contract.** crtr's CLI and
runtime primitives (`spawnChild`, `reviveNode`, `node message send`, etc.) are the only
sanctioned way to mutate canvas state. Documenting the byte-shape below is
for a consumer that needs to *read* or *reason about* the database (e.g.
recovering a home's identity after a guest recreate), not for direct SQL
writes.

- Opened with Node's built-in `node:sqlite` `DatabaseSync`.
- WAL mode enabled by `openDb()`.
- Forward-only migrations gated by `PRAGMA user_version`, currently through
  **v12**. A consumer pinning this contract should treat `user_version` as
  the schema version to check, not assume a fixed column set never changes
  going forward — this note describes the shape AS OF v12 and will need a later compatibility document if the schema changes in a way external readers depend on.
- **`nodes` table** — identity columns (`id`, and the fields mirrored from
  each node's `meta.json`) are derived from `nodes/<id>/meta.json` on disk;
  runtime columns (status, lifecycle, etc.) are row-authoritative *while the
  local canvas.db exists*, but are **not recoverable from a wiped local DB**
  — a rebuild re-derives identity from meta.json but cannot resurrect
  transient runtime state that only ever lived in the row.
- **`subscribes_to`** — DB-authoritative edge table (the push/subscription
  spine). Not derived from any on-disk file; a wiped DB loses these edges.
- **`spawned_by`** — provenance/audit edges, **re-derived from each node's
  `meta.json` during a rebuild** (unlike `subscribes_to`, this one survives a
  DB wipe as long as the node directories/meta.json files survive).
- **Auxiliary tables**: `focuses`, `crons`, `cron_runs`, `canvas_meta`.

A consumer pinning this contract should re-verify §6 against a live crtr
install (`PRAGMA user_version`, `.schema`) whenever bumping its pinned crtr
version, rather than assuming this note tracks every schema change.
