trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
227 lines • 8.01 kB
TypeScript
/**
* Peer Sync — Type Definitions
*
* DESIGN.md §3.5, §10.5 — Peer sync + CRDTs.
* Types for peer identity, sync messages, causal DAG, and
* branch concurrency modes.
*/
import type { VcsOp } from '../vcs/types.js';
export interface PeerId {
/** Unique peer identifier (typically derived from identity DID). */
id: string;
/** Human-readable display name. */
name: string;
/** Last seen timestamp. */
lastSeen?: string;
}
/**
* Wire protocol version. Bumped when the message shape or semantics change
* in a way that older peers cannot safely ignore. All outbound messages
* carry this value; receivers reject anything outside the supported range
* with a `protocol-version` nack.
*/
export declare const PROTOCOL_VERSION: 1;
export declare const MIN_SUPPORTED_VERSION: 1;
export declare const MAX_SUPPORTED_VERSION: 1;
export type SyncMessage = SyncHaveMessage | SyncWantMessage | SyncOpsMessage | SyncAckMessage | SyncNackMessage | SyncSnapshotRequestMessage | SyncSnapshotMessage | SyncGraphSnapshotMessage | SyncLaneJournalMessage | SyncDecisionTraceMessage | SyncEntityDeltaMessage | SyncDeviceRevokedMessage;
export type SyncMessageHandler = (message: SyncMessage) => void | Promise<void>;
/**
* Categorical reasons a receiver may reject ops sent over the wire.
*
* Engine-level reasons (`invalid-kind`, `hash-mismatch`, `missing-dependency`,
* `apply-failed`) mirror `IntegrateOpRejectReason` so engine rejections flow
* back to the sender unchanged. Wire-level reasons (`protocol-version`)
* describe failures detected by the sync layer itself. The union is kept
* separate so the wire protocol does not drift with engine internals.
*/
export type NackReason = 'invalid-kind' | 'hash-mismatch' | 'missing-dependency' | 'apply-failed' | 'protocol-version' | 'destructive-op' | 'bulk-delete' | 'system-modification' | 'quarantine-required' | 'rate_limited';
/** Advertise which op hashes we have. */
export interface SyncHaveMessage {
version: number;
type: 'have';
peerId: string;
/** Our head op hashes (one per branch). */
heads: Record<string, string>;
/** Total op count for quick comparison. */
opCount: number;
}
/** Request ops we're missing. */
export interface SyncWantMessage {
version: number;
type: 'want';
peerId: string;
/** Op hashes we need (those the remote has but we don't). */
wantHashes: string[];
/** Alternatively: request all ops after a given hash. */
afterHash?: string;
/**
* When set with empty `wantHashes` and no `afterHash`, request a truncated
* tail snapshot instead of the full room log (late-joiner catch-up).
*/
maxOps?: number;
}
/** Request a truncated tail snapshot from a room peer. */
export interface SyncSnapshotRequestMessage {
version: number;
type: 'sync-snapshot';
peerId: string;
/** Max ops in the tail. Default chosen by the room (usually 500). */
maxOps?: number;
}
/**
* Room response: recent tail of the canonical log plus metadata.
* Clients integrate `ops` like a normal `ops` message.
*/
export interface SyncSnapshotMessage {
version: number;
type: 'snapshot';
peerId: string;
/** Head hash of the full room log (may be beyond the returned tail). */
headHash?: string;
/** Total ops in the room canonical log. */
opCount: number;
/** True when `ops` is a truncated tail, not the full log. */
truncated: boolean;
ops: VcsOp[];
}
/** Send a batch of ops. */
export interface SyncOpsMessage {
version: number;
type: 'ops';
peerId: string;
ops: VcsOp[];
}
/** Acknowledge receipt. */
export interface SyncAckMessage {
version: number;
type: 'ack';
peerId: string;
/** Hashes of ops we've integrated (or already had). */
integrated: string[];
}
/**
* Reject one or more ops. Grouped by `reason` so a single batch may produce
* several nacks if rejections fall into multiple categories.
*/
export interface SyncNackMessage {
version: number;
type: 'nack';
peerId: string;
/** Hashes of ops that were rejected for this reason. */
refs: string[];
reason: NackReason;
/** Optional human-readable detail for logs. */
details?: string;
}
/**
* SQLite database snapshot for full graph state sync.
* Contains the complete graph database as a binary snapshot.
*/
export interface SyncGraphSnapshotMessage {
version: number;
type: 'graph-snapshot';
peerId: string;
/** SHA-256 hash of the snapshot data. */
snapshotHash: string;
/** Base64-encoded SQLite database snapshot. */
snapshotData: string;
/** Total entity count in snapshot. */
entityCount: number;
/** Snapshot timestamp. */
timestamp: string;
}
/**
* Lane-specific op journal for multi-agent isolation.
* Syncs lane journals separately from integration ops.
*/
export interface SyncLaneJournalMessage {
version: number;
type: 'lane-journal';
peerId: string;
/** Lane ID (UUID). */
laneId: string;
/** Head hash of this lane's journal. */
headHash: string;
/** JSONL-encoded lane journal ops. */
journalData: string;
/** Total ops in this lane. */
opCount: number;
}
/**
* Decision trace history for audit trail sync.
* Syncs structured decision traces between environments.
*/
export interface SyncDecisionTraceMessage {
version: number;
type: 'decision-trace';
peerId: string;
/** Decision trace ID. */
traceId: string;
/** JSON-encoded decision trace data. */
traceData: string;
/** Associated issue/operation ID. */
relatedOpId?: string;
}
/**
* Incremental entity delta for efficient graph updates.
* Contains only changed entities (add/modify/delete).
*/
export interface SyncEntityDeltaMessage {
version: number;
type: 'entity-delta';
peerId: string;
/** Delta hash for deduplication. */
deltaHash: string; /** Base snapshot hash this delta applies to. */
baseSnapshotHash: string;
/** JSON-encoded entity changes. */
entityData: string;
/** Number of entities in this delta. */
entityCount: number;
/** Change types present: add, modify, delete. */
changeTypes: ('add' | 'modify' | 'delete')[];
}
/**
* Device revocation signal (Slice D — docs/planning/device-registry-and-
* sprite-pairing.md). Sent by the revoking machine so clones and sprites
* update their local registries; the resolver then fails closed on the
* revoked key (ADR 0036 §2).
*/
export interface SyncDeviceRevokedMessage {
version: number;
type: 'device-revoked';
peerId: string;
/** The revoked device id (as registered under the identity). */
deviceId: string;
/** Identity the device was paired under. */
identityEntityId: string;
/** Device id of the revoking machine (root for CLI revocation). */
revokedBy: string;
timestamp: string;
}
export interface SyncState {
/** Our peer identity. */
localPeerId: string;
/** Known peers and their head hashes. */
peerHeads: Map<string, Record<string, string>>;
/** Ops we've sent but not yet acknowledged. */
pendingAcks: Set<string>;
/** Last sync timestamp per peer. */
lastSync: Map<string, string>;
}
export interface BranchPolicy {
/** If true, only fast-forward appends (one writer). Default. */
linear: boolean;
}
export interface SyncTransport {
/** Send a message to a specific peer. */
send(peerId: string, message: SyncMessage): Promise<void>;
/** Register a handler for incoming messages. */
onMessage(handler: SyncMessageHandler): void;
/** List connected peers. */
peers(): PeerId[];
/** Connect to the sync endpoint (or to a peer, for per-peer transports like the WebSocket one). */
connect?(peerId?: string, url?: string): Promise<void>;
/** Disconnect from the sync endpoint (or from a peer). */
disconnect?(peerId?: string): Promise<void>;
}
//# sourceMappingURL=types.d.ts.map