# Relay 1.3.0
Relay is a reusable work-history feature, not a separate identity service. Mock2 emits events;
CPR packages the page and API. Every application keeps its own runtime, database, workspace
permissions and release cadence. Installing the framework does not expose a public work feed.
The complete shared interface is the implementation used by the
[approved interactive demo](https://mock2.fractionate.ai/demos/relay/). The demo uses simulated
events; an application uses its authorized workspace events through the same `Relay` component.
Do not replace it with a simplified task list, download-only reference panel or separate mock UI.
The canonical copy-pastable installation prompt is
[`Relay-App-Integration-Prompt.md`](https://mock2.fractionate.ai/core/cpr/Relay-App-Integration-Prompt.md),
distributed locally at `.mock2/standards/Relay-App-Integration-Prompt.md` with the Mock2/CPR 1.13.0
standards. Read this guide and the actual exported adapter interfaces with it. Publishing this
shared package and public demo does not install or mount Relay in any existing application;
each consuming app requires its own verified integration.
## Interface acceptance
| Area | Required behavior in the consuming app |
| --- | --- |
| Workspace | Top tabs, left work-item column and right cards/activity/detail pane, independent content scrolling inside host-supplied height, mobile list/detail navigation, no initial document scrolling |
| Metric cards | Running, today's work time, verified deployments and needs-input context; applicable cards filter the work list and All work resets it |
| Activity | Date-selectable Git-style grid; blue imported history, green native activity, labeled mixed days; navigable earlier windows and all selected-day events |
| Run costs | Executed/reused/prerequisite-blocked results, original source evidence, invalidation reason, queue and command durations; missing measurements remain unknown |
| Tasks | Investigation/Build, measured running/stale clocks, outcome/summary, current checkpoint, next step, needs-input text, comments, requests and history/evidence |
| Connections | Clickable task dependencies and design relationships shown as a graph, including linked work and the tasks a prerequisite unblocks |
| Mockups | Reference gallery with linked-task context, image thumbnails and authenticated preview/download; unsupported inline formats remain safe downloadable evidence |
| Execution | Actual parallel steps, animated running connectors, retained failure/repair/retry attempts, command/findings/revision/reference inspection and replay of recorded history |
| Idea to outcome | Secondary view with visually connected origins, combined feature, contributing runs and verified delivery; distinct idea lead time and measured unique-run work time |
| Daily reports | Separate structured reports with deployed, still-active and needs-input context, with links back to recorded tasks |
| Theme and device | Host light/dark tokens, keyboard access, readable status contrast and desktop/mobile layouts preserving the same features |
An integration is complete only after these behaviors are checked in the rendered host app. A
successful build, an overflow check or a screenshot saved without comparison does not establish
interface parity. Keep a feature matrix and compare screenshots of the host against the approved
demo at desktop and mobile widths, including both host themes where supported. Record deliberate
host theme differences and any feature gap; do not silently treat a reduced interface as complete.
## Install in an existing application
1. Inspect the application's actual framework, routes, auth/session middleware, workspace/RBAC,
theme, PostgreSQL connection/migration owner, private storage, realtime and deployment process.
Use an immutable release/commit-pinned copy of `core/relay` as `@cpr/relay` 1.3.0; this is a
source package, not a promise of a public npm registry package. A vendored/file dependency or
packaged release can use the application's existing dependency installation process. The
checked-in `lib/` entry points are compiled ESM with declarations; `src/` remains editable
source. Pin the exact source commit and package/archive SHA-256 in the application's records
and dependency lock. Verify the published release hashes before using the files, and keep
`src/`, `lib/`, styles and migrations from that one candidate. If source changes, rebuild
`lib/` from the same source before packaging. Never mix a new source tree with an older compiled
runtime or install from floating `main`. React, Express, Zod, Drizzle and Lucide are peer
dependencies; reuse compatible host versions and document any small boundary adapter. Do not
introduce a new auth service, independent database/backend or second ORM to install Relay.
2. Add `migrations/0000_jazzy_martin_li.sql` to the host's migration plan. It was generated from
`src/db/schema.ts` using Drizzle. It creates only `relay_streams` and `relay_events`, with no
existing-table rewrite. The host migration owner applies it before mounting the API. No user,
session or credential tables are created. Disabling/removing the routes and package retains
data. A reader downgrade is subject to the history compatibility requirements below.
3. Implement `RelayHost` in `src/routes/relay.ts`. Use the existing authorization resolver for
every workspace and permission, the existing CSRF/session or scoped bearer validation in
`checkMutation`, and private host storage for reference bytes. `authorize` returns null for
anonymous users and forbidden workspaces. Use a transaction-capable PostgreSQL adapter; a
transaction must run every statement on one checked-out connection and roll back on throw.
4. Mount the router **after** the host's session/bearer middleware:
```ts
import { createRelayRouter } from '@cpr/relay/server';
import { postgresStore } from '@cpr/relay/store';
app.use('/api/workspaces/:workspaceId/relay', createRelayRouter({
events: postgresStore(hostDatabaseAdapter),
authorize: hostRelayPermissionAdapter,
checkMutation: hostCsrfOrScopedBearerAdapter,
storage: hostPrivateReferenceAdapter,
log: error => hostLogger.error(error),
}));
```
The adapter names above are application-owned implementations of the exported interfaces, not
claims that an existing SDK exports those names. See the executable PGlite adapter and fake host
in `tests/relay.test.ts` for the complete contract exercised with real SQL and HTTP requests.
Grant `relay.record` only to authenticated event-producing workers. Human pages need `relay.view`
and their allowed comment/request/reference permissions. Grant `relay.history` separately to
authorized workspace history operators for import/resume; do not grant human pages `relay.record`
to enable that control. The server enforces these permissions independently
of visible controls. SSE rechecks access on every poll and resumes with Last-Event-ID or `after`.
5. Mount the page in the host router/navigation under `/work`:
```tsx
import { Relay, createRelayClient } from '@cpr/relay';
import '@cpr/relay/styles.css';
// Memoize this client per workspace; changing identity reloads and unsubscribes the old stream.
const relay = createRelayClient(`/api/workspaces/${workspaceId}/relay`, {
request: hostJsonRequest, // credentials + CSRF, parses JSON, throws user-facing HTTP errors
subscribe: hostLiveStream, // parses SSE JSON, reconnects with cursor, returns an unsubscribe
history: true, // only when the host has configured history endpoints
canImportHistory: canHistory, // host grant for relay.history; server enforces it again
});
return
;
```
Use a `useMemo`/module stable client, not a new object each render. Map the manifest theme tokens
to the host palette in both light and dark modes; retain status accents and the component's
information hierarchy. Keep the host page shell and navigation. `example` is false by default.
The host supplies the available height; list and content panes scroll inside Relay.
For a full-viewport route, the optional `observeRelayViewport(hostElement)` helper updates only
the host element's `--relay-viewport-height` property. Call its returned cleanup function when the
host unmounts; it does not lock or scroll the document.
The bundled Vite example explicitly uses simulated events. Demo fixtures are opt-in development
data, visibly labeled, never the default production workspace feed and never a fallback that hides
an auth, connection or empty-data problem.
The host transport must parse JSON failures, preserve same-origin credentials, supply CSRF for
session-authenticated mutations (or validate the existing scoped bearer mechanism), parse SSE,
reconnect from the last persisted cursor and unsubscribe on workspace/identity change. Do not put
tokens in URLs or browser storage merely to enable the stream. Configure the existing reverse
proxy to avoid buffering this authenticated SSE route. Test access revocation after the stream
opens; the server rechecks access, and the client must reflect disconnection rather than claiming
live work.
Reference bytes stay in the host's private storage and are served through workspace-authorized
routes. Gallery preview access has the same workspace boundary as downloads; public object URLs
are not a shortcut. `RelayClient.preview(artifactId)` is optional; `createRelayClient` supplies the
same-origin `/references/:artifactId/preview` URL. That route rechecks authorization and image
signatures and only serves PNG/JPEG/WebP inline. PDF, text and Markdown remain attachment-only
through `reference(artifactId)`. An `
` request uses the host session cookie; a bearer-only
app must use its existing authenticated proxy or fetch bytes with its authorized transport and
expose managed blob URLs, revoking them on replacement/unmount. Never embed a token in an image
URL or render Markdown/text as raw HTML. Check proxy/body size limits against the
configured reference limit, including JSON/base64 expansion; test real storage, missing-object
and denied-workspace cases in the app.
## Automatic initial Git lookback
Configure the repository on its authorized backend checkout using its stable host repository ID:
```sh
node .mock2/bin/relay.mjs init --workspace WORKSPACE_ID --repository-id REPOSITORY_ID
node .mock2/bin/relay.mjs history-status
node .mock2/bin/relay.mjs history --earlier
```
`init` (or `connect`) starts bounded background history work after saving the explicit source and
workspace mapping. `--repository PATH` selects a known authorized checkout; the browser never
supplies this path. `--foreground` is available for deterministic installation/test procedures.
An optional `--ingest-url HTTPS_URL` binds delivery to the configured host. The existing
`RELAY_INGEST_TOKEN` is supplied by the host secret mechanism and never persisted in configuration.
With a configured endpoint and token, the worker delivers after importing; otherwise history stays
local and status reports the delivery gap. Existing producer synchronization remains available.
Initial import observes the previous six months at a pinned commit. Work is batched and resumable;
`history --earlier` requests an earlier window. The background worker pauses after at most 100
batches (10,000 commits with its default batch size); resume the current window with `history`.
The status reports running, paused, complete or failed and flags shallow history explicitly.
After fetching deeper Git ancestry, run `history` again (or POST `/history/import` with
`{"earlier":false}`); a completed shallow window is rechecked at its pinned commit and deduplicated. Do not claim complete Git history from a shallow
checkout or from a window/cursor that has not finished. The host may inject the exported
`createLocalHistoryAdapter` from `@cpr/relay/history`, mapping an authorized workspace to its
configured producer root. The optional router history endpoints check workspace capabilities and
mutation protection; they never accept arbitrary filesystem paths. Hosts without local Git can
provide an equivalent adapter using their existing repository-provider connection.
A history observation carries repository ID, full SHA, parents, subject, original author/committer
timestamps and import time. Deduplication preserves a single observation per workspace/repository/
SHA. Blue imported squares and green native squares show origin; mixed days show both. Historical
tests, deployment, intent and execution time are unknown until independent recorded evidence is
linked. They do not contribute to measured work time or verified-deployment counts.
## Upgrade and retained-history compatibility
Relay 1.3.0 adds three optional `schemaVersion:3` lifecycle observations with minimum reader 1.3.0:
`record.added` (a request, interpretation, question, answer, feedback, proposed or approved decision,
acceptance check, risk, plan revision, authorization, finding or observation, with its author, provenance
and references), `work.state.changed` (one of nine lifecycle stages, a state, and a waiting reason when
waiting) and `execution.recorded` (a Mock2 execution record, or its id and SHA-256 digest). The repository
producer writes them only when `.mock2/checks.json` sets `"relay": {"lifecycleEvidence": true}`, off by
default exactly like `checkEvidence` (`relay.mjs lifecycle` reads `{type, taskId, data}` on standard input
and reports `emitted: false` with the reason while it is off). Upgrade every reader, ingestion validator
and producer to 1.3.0 before turning it on; version-1 and version-2 history remains valid unchanged, and a
rollback keeps a 1.3.0-compatible reader once version-3 observations exist. The event contract is
`Work-Lifecycle.md` in the standards directory.
Relay 1.2.0 adds `schemaVersion:2` `history.commit.observed` and `check.recorded` with minimum reader
1.2.0. Upgrade frontend/server/storage validators and repository producers together before enabling
these observations. Existing version-1 records remain valid without rewriting. Apply the additive
`migrations/0001_aspiring_morph.sql` migration through the host's migration owner. Rollback must
retain a 1.2.0-compatible reader or tested adapter; never remove version-2 history to satisfy an old
reader. The following version-1 metadata requirement still applies:
Within `schemaVersion: 1` records, the 1.1.0 metadata vocabulary requires a
**minimum reader version of 1.1.0 after enriched metadata events are written**. A 1.0.1 validator
rejects `checkpoint`, `next` or `needsInput` in `task.created` as unexpected fields, and rejects
`task.updated` as an unsupported event type. A preserved envelope version does not make those
older validators forward-compatible.
When upgrading an existing integration, update all frontend/server event readers, worker
consumers, and repository emitter/export/sync/check-capture packages to the pinned 1.3.0
implementation before enabling producers that emit this metadata. Test reads of existing history
and the first enriched event through every delivery path. An older producer emitting only the
previous vocabulary does not need its old events rewritten, but any tool that reads a ledger
containing enriched events needs the new reader.
Retain enriched database and repository history during disabling or rollback. Returning to the
1.0.1 package alone cannot read that history. A rollback must keep a 1.2.0-compatible reader once version-2 observations exist, or
provide a tested compatibility adapter that handles the new metadata/type while preserving
stored events and lifecycle semantics. Do not erase or strip history to make the old validator
pass. Record this minimum-reader requirement and the actual rollback path in the consuming app's
release information; route/package removal and compatible software downgrade are different acts.
## Work recording
The installed repository gets `.mock2/bin/relay.mjs`, the package under `.mock2/features/relay`,
and automatic check-runner capture. The check runner emits actual started / heartbeat / finished
records including the check's exit result and revision. Missing checks remain `missing`, failures
remain failures; recording cannot turn a failing check green. Heartbeats say only that the process
is running; they never invent a test count or a reviewer percentage. Raw command output is not
captured automatically because it may contain credentials or private application data.
For a task, instrument boundaries and meaningful checkpoints with JSON on stdin:
```sh
node .mock2/bin/relay.mjs emit < event.json
node .mock2/bin/relay.mjs export
```
```json
{"schemaVersion":1,"id":"event-unique-id","workspaceId":"my-workspace","taskId":"W42","runId":"run-unique-id","stepId":"implementation","attempt":1,"type":"step.started","at":"2026-10-02T16:00:00.000Z","data":{"title":"Implementation","revision":"exact-git-sha","dependencies":[]}}
```
Create `task.created` and `run.started` first. Follow with `step.activity` and `step.finished`,
then `run.finished`. Use a distinct run ID per execution; retry a failed logical step with the
same step ID and a higher attempt. Dependencies reference `stepId:attempt`; a repair may depend
on a failed attempt and its new implementation may depend on the repair. All remain in history.
The shared validator lists accepted event fields and never accepts executable event instructions.
`task.created` can additionally record `checkpoint`, `next` and `needsInput`. Later
`task.updated` events update the summary and those display fields without rewriting the
original record. For example:
```json
{"schemaVersion":1,"id":"checkpoint-unique-id","workspaceId":"my-workspace","taskId":"W42","type":"task.updated","at":"2026-10-02T16:04:00.000Z","data":{"checkpoint":"Export schema verified","next":"Build the download action","needsInput":"Confirm which columns should be exported"}}
```
Use `"needsInput":""` in a later `task.updated` to clear the displayed input request when it is
resolved. `task.updated` requires at least one of `summary`, `checkpoint`, `next` or `needsInput`,
each a string of at most 8,000 characters; an empty string clears that field. It targets a task
that already exists in the same workspace and does not accept run/step context, status, revision
or control fields. Metadata is not a run heartbeat and cannot start, finish, pause, move to backlog or
prioritize a worker. Run lifecycle and accepted worker acknowledgments remain authoritative.
Use meaningful checkpoint messages rather than repeated task titles; time-only heartbeats should
not pretend to be findings. Daily reports summarize actual records and link to their tasks; a
report is not a separate execution run.
Set `MOCK2_WORKSPACE_ID`, `MOCK2_TASK_ID` and optionally `MOCK2_RUN_ID` for check capture. Without
them checks use a local `repository` / `verification` task and a unique run. With an explicit `MOCK2_RUN_ID`,
the runner records its checks but never closes the agent-owned run; its owner emits the finish. An agent supplies its
own started/activity/finished events using the same emitter; installing this does not instrument
unmodified external coding tools or reconstruct missing historical checkpoints.
`state/relay/events` is an append-only directory of sequence-prefixed event files. Atomic rename
and a short writer lock avoid interleaved writes; retries of the same ID are idempotent. Commit
that directory beside the existing Markdown ledger. A crashed writer can leave `.writer-lock`;
verify no writer is active before removing only that lock. No timeout steals a live lock.
To send recorded work to a host, set `RELAY_INGEST_URL` to the HTTPS workspace `/events` endpoint
and obtain `RELAY_INGEST_TOKEN` through the host credential mechanism, then run
`node .mock2/bin/relay.mjs sync`. No token is committed or returned. Re-sending the ledger is
idempotent; redirects are refused. The host token's scope must match `workspaceId`. Offline
recording works without a host. `sync` is an explicit retry/delivery step; configured initial
lookback also delivers through the same authenticated channel. There is no unconfigured export
or continuously installed background service.
Plan the app's actual producers and delivery path during installation. Connect its existing
coding/check/deployment workflow to the emitter, record a real task through at least one check,
then demonstrate explicit authenticated `sync` into the correct workspace. Preserve the Markdown
run ledger and existing historical records. Older start/end records can establish only their
recorded overall duration/status; do not manufacture intermediate steps, progress or idea dates.
Where a producer has no instrumentation, show that gap rather than substituting demo events.
## Verification timing and reuse
The runner writes one per-run JSON evidence record and emits `check.recorded` observations. Run
costs distinguish executed results, unchanged-input reuse and prerequisites that could not run,
including queue time and actual command duration. Original evidence time and identity remain
visible. No new step execution is fabricated for reuse. Human work/handoff records reference the
canonical JSON and add scope, decisions and review findings. See `Standards-Benchmark.md` for
repeatable runner measurements and the separate fresh-agent build comparison procedure.
## Meaning and controls
- A task is an Investigation or Build; reports are separate records. A deployment card counts
only `deployment.recorded` with verified=true, category feature/update/patch and revision.
- Work time sums distinct runs, not overlapping steps. Simultaneous runs count separately and
are not billed inference time. Day boundaries use the selected timezone, including DST.
- Idea lead time measures the recorded idea's inception to the recorded verified outcome (or the
present while still open). It includes waiting. Work time sums each contributing run once even
when multiple origins link to it. Neither metric estimates API billing or missing history.
- Running clocks become stale after `staleAfterMs` (default 120 seconds); stale time stops at the
last observed activity. An open page is not proof that a worker remains active.
- Idea IDs survive combinations. `relationship.added` with `inspired` or `combines` points from
source idea/task to outcome task. Lineage counts each contributing run once. `depends_on` points
from a prerequisite to its dependent task. A `design` link goes from a recorded artifact ID to
its destination task ID, with event.taskId equal to that destination. Both must exist in the same
workspace. Linked references appear under Linked mockups on the destination task.
- A browser sends only `control.requested`. Worker integration validates and acknowledges the
request with `control.acknowledged`, requestId, action and accepted. Only accepted acknowledgment
changes the effective task state. This package does not run shell commands or stop workers.
- References accept PNG/JPEG/WebP/PDF/UTF-8 text/Markdown, validate extension and signature, and
are served as private attachments with nosniff. No scripts or SVG. Maximum size is host
configurable (default 5 MiB). Failed event persistence may leave an unreferenced host object;
the host may reconcile orphan objects separately. Nothing automatically deletes references.
- Redaction catches common bearer/token/password patterns, not arbitrary secrets. Producers must
summarize activity and redact sensitive evidence before emitting it. The authenticated feed is
not automatically suitable for external sharing.
## Verification and limits
`npm ci`, `npm run build:package`, `npm run check:types`, `npm test`, `npm run build:example`, and
`node scripts/browser-check.mjs` exercise the package locally. The browser check starts/stops its
own loopback server. PGlite tests execute the generated PostgreSQL schema; a consuming host must
also run its integration tests on its PostgreSQL version and real auth/realtime/storage adapters.
Host acceptance includes authenticated/anonymous and cross-workspace access, permission-specific
mutations, CSRF/scoped bearer rejection, SSE reconnect without duplicate events and access
revocation, real upload/preview/download validation, pending/rejected/accepted control requests,
event ingestion/retry idempotence and data retained after disabling. Run the app's normal checks
as well as the interface matrix above. Save actual rendered comparisons with the candidate
identity, viewport, theme, fixture/real-data source and result. Check the public demo and host
against the same package identity. Record an unavailable integration check as unavailable, not
passed; passing package tests alone does not prove host integration.
Live causal transitions remain validated against the workspace stream. History imports use batched
local append and a unique indexed history identity in the reference database, avoiding per-commit
replay of the full live projection. Very large streams still require host capacity measurement. SSE uses a configurable database poll interval
(default 1 second), not a required second message bus. Persisted sequence order, independent of
client timestamps, is the reconnect cursor. The package does not claim automatic adoption by apps
that have not mounted it or automatic execution control over workers without acknowledgments.
## Automatic producer delivery (1.2.1)
See the distributed `Relay-Recording.md` for event semantics, command/session wrapping, automatic
configured emitter/check delivery, retrying structured-event watch, delivery status and actual
coding-tool coverage. `relay.mjs run --title "Coding session" -- COMMAND ARGS` records real process
boundaries using version-1 lifecycle events; it does not prove feature acceptance or deployment.
The host must acknowledge the same event ID/workspace with its committed sequence. Failed delivery
retains local records. Existing 1.1.x hosts still need migration before version-2 observations.