@slopus/happy-wire
v0.1.0
Published
Shared message wire types and Zod schemas for Happy clients and services
Downloads
731
Readme
@slopus/happy-wire
Canonical wire specification package for Happy clients and services.
This package defines shared wire contracts as TypeScript types + Zod schemas. It is intentionally small and focused on protocol-level data only.
Quick Examples (Legacy vs New)
Both legacy and new formats are transported inside encrypted session messages.
Legacy format examples (decrypted payload):
{
"role": "user",
"content": {
"type": "text",
"text": "fix the failing test"
},
"meta": {
"sentFrom": "mobile"
}
}{
"role": "agent",
"content": {
"type": "output",
"data": {
"type": "message",
"message": "I found the issue in api/session.ts"
}
},
"meta": {
"sentFrom": "cli"
}
}New session protocol format example (decrypted payload):
{
"role": "session",
"content": {
"id": "msg_01",
"time": 1739347230000,
"role": "agent",
"turn": "turn_01",
"ev": {
"t": "text",
"text": "I found the issue in api/session.ts"
}
},
"meta": {
"sentFrom": "cli"
}
}Modern session protocol user envelope (decrypted payload):
{
"role": "session",
"content": {
"id": "msg_legacy_user_01",
"time": 1739347231000,
"role": "user",
"ev": {
"t": "text",
"text": "fix the failing test"
}
},
"meta": {
"sentFrom": "cli"
}
}Protocol invariant:
- outer
role = "session"marks modern session-protocol payloads. - inside
content, enveloperoleis only"user"or"agent".
Session protocol send rollout (ENABLE_SESSION_PROTOCOL_SEND):
- sender emits modern session-protocol user payloads (
role = "session"withcontent.role = "user"). - default (disabled): app consumes legacy user payloads (
role = "user",content.type = "text") and drops modern user payloads. - enabled: app consumes modern user payloads and drops legacy user payloads.
- truthy values:
1,true,yes(case-insensitive).
Wire-level encrypted container (same for legacy and new):
{
"id": "msg-db-row-id",
"seq": 101,
"localId": null,
"content": {
"t": "encrypted",
"c": "BASE64_ENCRYPTED_PAYLOAD"
},
"createdAt": 1739347230000,
"updatedAt": 1739347230000
}Purpose
@slopus/happy-wire centralizes definitions for:
- encrypted message/update payloads
- session protocol envelope and event stream
- helper for creating valid session envelopes
The goal is to keep CLI/app/server/agent on the same wire contract and avoid schema drift.
Package Identity
- Name:
@slopus/happy-wire - Workspace path:
packages/happy-wire - Entry:
src/index.ts - Runtime deps:
zod,@paralleldrive/cuid2
Public Exports
src/index.ts exports everything from:
src/messages.tssrc/legacyProtocol.tssrc/sessionProtocol.ts
messages.ts exports
Schemas + inferred types:
SessionMessageContentSchemaSessionMessageSessionMessageSchemaMessageMetaSchemaMessageMetaSessionProtocolMessageSchemaSessionProtocolMessageMessageContentSchemaMessageContentVersionedEncryptedValueSchemaVersionedEncryptedValueVersionedNullableEncryptedValueSchemaVersionedNullableEncryptedValueUpdateNewMessageBodySchemaUpdateNewMessageBodyUpdateSessionBodySchemaUpdateSessionBodyVersionedMachineEncryptedValueSchemaVersionedMachineEncryptedValueUpdateMachineBodySchemaUpdateMachineBodyCoreUpdateBodySchemaCoreUpdateBodyCoreUpdateContainerSchemaCoreUpdateContainer
Compatibility aliases:
ApiMessageSchema->SessionMessageSchemaApiMessage->SessionMessageApiUpdateNewMessageSchema->UpdateNewMessageBodySchemaApiUpdateNewMessage->UpdateNewMessageBodyApiUpdateSessionStateSchema->UpdateSessionBodySchemaApiUpdateSessionState->UpdateSessionBodyApiUpdateMachineStateSchema->UpdateMachineBodySchemaApiUpdateMachineState->UpdateMachineBodyUpdateBodySchema->UpdateNewMessageBodySchemaUpdateBody->UpdateNewMessageBodyUpdateSchema->CoreUpdateContainerSchemaUpdate->CoreUpdateContainer
legacyProtocol.ts exports
Schemas + inferred types:
UserMessageSchemaUserMessageAgentMessageSchemaAgentMessageLegacyMessageContentSchemaLegacyMessageContent
sessionProtocol.ts exports
Schemas + inferred types:
sessionRoleSchemaSessionRolesessionTextEventSchemasessionServiceMessageEventSchemasessionToolCallStartEventSchemasessionToolCallEndEventSchemasessionFileEventSchemasessionTurnStartEventSchemasessionStartEventSchemasessionTurnEndStatusSchemaSessionTurnEndStatussessionTurnEndEventSchemasessionStopEventSchemasessionEventSchemaSessionEventsessionEnvelopeSchemaSessionEnvelopeCreateEnvelopeOptionscreateEnvelope(...)
Wire Type Specifications
Common Primitive Rules
These are schema-level requirements, not just recommendations.
id,sid,machineId,call,name,title,description,ref:stringseq,createdAt,updatedAt,size,width,height,version,activeAt:number- All nullable fields are explicitly marked with
.nullable(). - All optional fields are explicitly marked with
.optional(). .nullish()meansundefined | null | <type>.
Message/Update Specs (messages.ts)
SessionMessageContentSchema
{
t: 'encrypted';
c: string;
}Meaning:
tis a strict discriminator with value'encrypted'.cis encrypted payload bytes encoded as a string (typically base64 in current usage).
SessionMessageSchema
{
id: string;
seq: number;
localId?: string | null;
content: SessionMessageContent;
createdAt: number;
updatedAt: number;
}Notes:
localIdis.nullish()for compatibility with different producers.createdAtandupdatedAtare required in this shared schema.
MessageMetaSchema
{
sentFrom?: string;
permissionMode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'read-only' | 'safe-yolo' | 'yolo';
model?: string | null;
fallbackModel?: string | null;
customSystemPrompt?: string | null;
appendSystemPrompt?: string | null;
allowedTools?: string[] | null;
disallowedTools?: string[] | null;
displayText?: string;
}Legacy Decrypted Payload Specs (legacyProtocol.ts)
UserMessageSchema (legacy decrypted payload)
{
role: 'user';
content: {
type: 'text';
text: string;
};
localKey?: string;
meta?: MessageMeta;
}AgentMessageSchema (legacy decrypted payload)
{
role: 'agent';
content: {
type: string;
[key: string]: unknown;
};
meta?: MessageMeta;
}LegacyMessageContentSchema
Discriminated union on role:
'user'->UserMessageSchema'agent'->AgentMessageSchema
Top-Level Decrypted Payload Specs (messages.ts)
SessionProtocolMessageSchema (modern decrypted payload wrapper)
{
role: 'session';
content: SessionEnvelope;
meta?: MessageMeta;
}MessageContentSchema
Discriminated union on top-level role:
'user'->UserMessageSchema(legacy)'agent'->AgentMessageSchema(legacy)'session'->SessionProtocolMessageSchema(modern)
Message/Update Specs (messages.ts) Continued
VersionedEncryptedValueSchema
{
version: number;
value: string;
}Used for encrypted, version-tracked blobs that cannot be null when present.
VersionedNullableEncryptedValueSchema
{
version: number;
value: string | null;
}Used where payload presence can be intentionally reset to null while still versioning.
VersionedMachineEncryptedValueSchema
{
version: number;
value: string;
}Machine update variant. Equivalent shape to VersionedEncryptedValueSchema.
UpdateNewMessageBodySchema
{
t: 'new-message';
sid: string;
message: SessionMessage;
}UpdateSessionBodySchema
{
t: 'update-session';
id: string;
metadata?: VersionedEncryptedValue | null;
agentState?: VersionedNullableEncryptedValue | null;
}Important distinction:
metadata.valueisstringwhen metadata block exists.agentState.valuemay bestringornullwhen block exists.
UpdateMachineBodySchema
{
t: 'update-machine';
machineId: string;
metadata?: VersionedMachineEncryptedValue | null;
daemonState?: VersionedMachineEncryptedValue | null;
active?: boolean;
activeAt?: number;
}CoreUpdateBodySchema
Discriminated union on t with exactly 3 variants:
'new-message''update-session''update-machine'
CoreUpdateContainerSchema
{
id: string;
seq: number;
body: CoreUpdateBody;
createdAt: number;
}Session Protocol Specs (sessionProtocol.ts)
Role
sessionRoleSchema
'user' | 'agent'Role meaning:
'user': user-originated envelope.'agent': agent-originated envelope.
Event Variants
sessionEventSchema is a discriminated union on t with 9 variants.
1) Text event
{
t: 'text';
text: string;
thinking?: boolean;
}2) Service event
{
t: 'service';
text: string;
}3) Tool-call-start event
{
t: 'tool-call-start';
call: string;
name: string;
title: string;
description: string;
args: Record<string, unknown>;
}4) Tool-call-end event
{
t: 'tool-call-end';
call: string;
}5) File event
{
t: 'file';
ref: string;
name: string;
size: number;
image?: {
width: number;
height: number;
thumbhash: string;
};
}6) Turn-start event
{
t: 'turn-start';
}7) Start event
{
t: 'start';
title?: string;
}8) Turn-end event
{
t: 'turn-end';
status: 'completed' | 'failed' | 'cancelled';
}9) Stop event
{
t: 'stop';
}Envelope
sessionEnvelopeSchema
{
id: string;
time: number;
role: 'user' | 'agent';
turn?: string;
subagent?: string; // must pass cuid2 validation when present
ev: SessionEvent;
}Additional validation (superRefine):
- If
ev.t === 'service', thenroleMUST be'agent'. - If
ev.t === 'start'orev.t === 'stop', thenroleMUST be'agent'. - If
subagentis present, it MUST satisfyisCuid(...).
Helper Function Contract
createEnvelope(role, ev, opts?)
Input:
role: SessionRoleev: SessionEventopts?: { id?: string; time?: number; turn?: string; subagent?: string }
Behavior:
- If
opts.idis absent, generates id usingcreateId(). - If
opts.timeis absent, setstimetoDate.now(). - Includes
turnonly when provided. - Includes
subagentonly when provided.
Output:
- Returns a
SessionEnvelopeparsed bysessionEnvelopeSchema. - Throws on invalid combinations (for example
role = 'user'withev.t = 'service').
Normative JSON Examples
Update container with new-message
{
"id": "upd-1",
"seq": 100,
"createdAt": 1739347200000,
"body": {
"t": "new-message",
"sid": "session-1",
"message": {
"id": "msg-1",
"seq": 55,
"localId": null,
"content": {
"t": "encrypted",
"c": "Zm9v"
},
"createdAt": 1739347199000,
"updatedAt": 1739347199000
}
}
}Decrypted new-message content example
message.content.c (ciphertext) decrypts into the payload below for a session-protocol message:
{
"role": "session",
"content": {
"id": "env_01",
"time": 1739347232000,
"role": "agent",
"turn": "turn_01",
"ev": {
"t": "text",
"text": "I found 3 TODOs."
}
},
"meta": {
"sentFrom": "cli"
}
}For user text migration behavior:
- clients emit only the modern payload (
role = "session"withcontent.role = "user"). - if
ENABLE_SESSION_PROTOCOL_SENDis disabled, app keeps consuming legacy payloads and drops modern payloads. - if
ENABLE_SESSION_PROTOCOL_SENDis enabled, app consumes modern payloads and drops legacy payloads.
Update container with update-session
{
"id": "upd-2",
"seq": 101,
"createdAt": 1739347210000,
"body": {
"t": "update-session",
"id": "session-1",
"metadata": {
"version": 8,
"value": "BASE64..."
},
"agentState": {
"version": 13,
"value": null
}
}
}Update container with update-machine
{
"id": "upd-3",
"seq": 102,
"createdAt": 1739347220000,
"body": {
"t": "update-machine",
"machineId": "machine-1",
"metadata": {
"version": 2,
"value": "BASE64..."
},
"daemonState": {
"version": 3,
"value": "BASE64..."
},
"active": true,
"activeAt": 1739347220000
}
}Session protocol envelope
{
"id": "x8s1k2...",
"role": "agent",
"turn": "turn-42",
"ev": {
"t": "turn-start"
}
}Parsing/Validation Usage
import {
CoreUpdateContainerSchema,
sessionEnvelopeSchema,
} from '@slopus/happy-wire';
const maybeUpdate = CoreUpdateContainerSchema.safeParse(input);
if (!maybeUpdate.success) {
// invalid update payload
}
const maybeEnvelope = sessionEnvelopeSchema.safeParse(envelopeInput);
if (!maybeEnvelope.success) {
// invalid envelope/event payload
}Build and Distribution Specification
package.json contract:
main:./dist/index.cjsmodule:./dist/index.mjstypes:./dist/index.d.ctsexports["."]provides both CJS and ESM entrypoints with type paths.
Build script:
shx rm -rf dist && npx tsc --noEmit && pkgroll
Tests:
vitestagainstsrc/*.test.ts
Publish gate:
prepublishOnlyruns build + test
Published files:
distpackage.jsonREADME.md
Monorepo Build Dependency Behavior
In this repository, consumer workspaces import @slopus/happy-wire through package exports that point at dist/*.
That means on a clean checkout:
- Build wire first:
yarn workspace @slopus/happy-wire build - Then build/typecheck dependents.
After publishing to npm, dependents consume prebuilt artifacts from the published tarball.
Change Policy
When modifying wire schemas:
- Prefer additive changes to keep older consumers compatible.
- Treat discriminator values (
t) as protocol-level API and avoid breaking renames. - Document semantic changes in this README.
- Bump package version before downstream releases that depend on new schema behavior.
Development Commands
# from repository root
yarn workspace @slopus/happy-wire build
yarn workspace @slopus/happy-wire testRelease Commands (maintainers)
# interactive release target selection from repo root
yarn release
# direct release invocation
yarn workspace @slopus/happy-wire releaseThis prepares release artifacts using the same release-it flow as other publishable libraries in the monorepo.
