@agent-relay/sdk 10.4.0 → 10.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/bin/agent-relay-broker-win32-x64.exe +0 -0
  6. package/dist/agent-relay.d.ts +23 -0
  7. package/dist/agent-relay.d.ts.map +1 -1
  8. package/dist/agent-relay.js +57 -5
  9. package/dist/agent-relay.js.map +1 -1
  10. package/dist/delivery/types.d.ts +5 -1
  11. package/dist/delivery/types.d.ts.map +1 -1
  12. package/dist/listeners.d.ts.map +1 -1
  13. package/dist/listeners.js +16 -4
  14. package/dist/listeners.js.map +1 -1
  15. package/dist/messaging/event-fanin.d.ts +54 -0
  16. package/dist/messaging/event-fanin.d.ts.map +1 -0
  17. package/dist/messaging/event-fanin.js +293 -0
  18. package/dist/messaging/event-fanin.js.map +1 -0
  19. package/dist/messaging/index.d.ts +2 -0
  20. package/dist/messaging/index.d.ts.map +1 -1
  21. package/dist/messaging/index.js +2 -0
  22. package/dist/messaging/index.js.map +1 -1
  23. package/dist/messaging/normalize.d.ts.map +1 -1
  24. package/dist/messaging/normalize.js +479 -509
  25. package/dist/messaging/normalize.js.map +1 -1
  26. package/dist/messaging/observer-source.d.ts +80 -0
  27. package/dist/messaging/observer-source.d.ts.map +1 -0
  28. package/dist/messaging/observer-source.js +502 -0
  29. package/dist/messaging/observer-source.js.map +1 -0
  30. package/dist/messaging/relaycast-client.d.ts +2 -1
  31. package/dist/messaging/relaycast-client.d.ts.map +1 -1
  32. package/dist/messaging/relaycast-client.js.map +1 -1
  33. package/dist/messaging/types.d.ts +133 -107
  34. package/dist/messaging/types.d.ts.map +1 -1
  35. package/package.json +5 -3
@@ -1,42 +1,72 @@
1
+ import { AgentPresenceInfoSchema, AgentSchema, AgentStatusSchema, AgentTypeSchema, ChannelMemberInfoSchema, ChannelReadStatusSchema, ChannelSchema, CreateAgentResponseSchema, CreateGroupDmResponseSchema, DeliveryMessageSchema, DeliverySchema, DeliveryStatusSchema, InboxResponseSchema, MessageInjectionModeSchema, MessageSchema, MessageWithMetaSchema, ReactionGroupSchema, ReadReceiptSchema, ReaderInfoSchema, SearchMessageResultSchema, } from '@relaycast/types';
2
+ import { z } from 'zod';
1
3
  function isRecord(value) {
2
4
  return value !== null && typeof value === 'object' && !Array.isArray(value);
3
5
  }
4
- function readValue(record, ...keys) {
5
- if (!record)
6
- return undefined;
7
- for (const key of keys) {
8
- if (Object.hasOwn(record, key))
9
- return record[key];
6
+ // ── Wire boundary ────────────────────────────────────────────────────────────
7
+ //
8
+ // `@relaycast/sdk` camelizes wire responses at runtime, while the canonical
9
+ // contract in `@relaycast/types` (and injected raw clients) is snake_case.
10
+ // Instead of probing both spellings field by field, every payload is folded
11
+ // back onto the canonical snake_case wire shape once, then validated with a
12
+ // schema derived from `@relaycast/types` and mapped deliberately.
13
+ /** Keys whose values are caller-defined payloads that must pass through untouched. */
14
+ const PASSTHROUGH_KEYS = new Set([
15
+ 'metadata',
16
+ 'blocks',
17
+ 'data',
18
+ 'value',
19
+ 'input',
20
+ 'output',
21
+ 'parameters',
22
+ 'headers',
23
+ ]);
24
+ function toSnakeKey(key) {
25
+ return key.replace(/[A-Z]/g, (char, index) => (index > 0 ? '_' : '') + char.toLowerCase());
26
+ }
27
+ function toWire(value) {
28
+ if (Array.isArray(value))
29
+ return value.map(toWire);
30
+ if (!isRecord(value))
31
+ return value;
32
+ // Null-prototype output: an untrusted `__proto__` key (own on JSON.parse'd
33
+ // payloads) stays an ordinary data property instead of hijacking the
34
+ // prototype and feeding inherited fields to downstream reads.
35
+ const out = Object.create(null);
36
+ for (const [key, val] of Object.entries(value)) {
37
+ const snake = toSnakeKey(key);
38
+ out[snake] = PASSTHROUGH_KEYS.has(snake) ? val : toWire(val);
10
39
  }
11
- return undefined;
40
+ return out;
12
41
  }
13
- function readString(record, ...keys) {
14
- const value = readValue(record, ...keys);
15
- if (typeof value === 'string')
16
- return value;
17
- if (typeof value === 'number' || typeof value === 'bigint')
18
- return String(value);
19
- return undefined;
42
+ /** Snake-case a payload's keys and validate it against a canonical-derived schema. */
43
+ function parseWire(schema, input) {
44
+ const wired = toWire(input);
45
+ return schema.parse(isRecord(wired) ? wired : {});
20
46
  }
21
- function readNullableString(record, ...keys) {
22
- const value = readValue(record, ...keys);
23
- return typeof value === 'string' ? value : undefined;
47
+ /** Drop undefined values so optional fields stay absent instead of present-but-undefined. */
48
+ function compact(value) {
49
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
24
50
  }
25
- function readBoolean(record, ...keys) {
26
- const value = readValue(record, ...keys);
27
- return typeof value === 'boolean' ? value : undefined;
51
+ /** Collapse empty/null strings to undefined (absent). */
52
+ function opt(value) {
53
+ return value ? value : undefined;
54
+ }
55
+ function str(record, key) {
56
+ const value = record[key];
57
+ return typeof value === 'string' ? value : undefined;
28
58
  }
29
- function readNumber(record, ...keys) {
30
- const value = readValue(record, ...keys);
59
+ function num(record, key) {
60
+ const value = record[key];
31
61
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
32
62
  }
33
- function readArray(record, ...keys) {
34
- const value = readValue(record, ...keys);
35
- return Array.isArray(value) ? value : [];
63
+ function bool(record, key) {
64
+ const value = record[key];
65
+ return typeof value === 'boolean' ? value : undefined;
36
66
  }
37
- function readRecord(record, ...keys) {
38
- const value = readValue(record, ...keys);
39
- return isRecord(value) ? { ...value } : undefined;
67
+ function rec(record, key) {
68
+ const value = record[key];
69
+ return isRecord(value) ? value : {};
40
70
  }
41
71
  function normalizeOptionalChannelName(value) {
42
72
  if (!value)
@@ -46,384 +76,369 @@ function normalizeOptionalChannelName(value) {
46
76
  export function normalizeChannelName(value) {
47
77
  return normalizeOptionalChannelName(value) ?? value;
48
78
  }
49
- function normalizeAgentType(value) {
50
- return value === 'human' || value === 'system' ? value : 'agent';
51
- }
52
- function normalizeAgentStatus(value) {
53
- if (value === 'online' || value === 'offline' || value === 'away')
54
- return value;
55
- return 'unknown';
56
- }
57
- function normalizeOnlineStatus(value) {
58
- return value === 'online' ? 'online' : 'offline';
59
- }
60
- function normalizeRole(value) {
61
- return value === 'owner' ? 'owner' : 'member';
62
- }
63
- function normalizeMode(value) {
64
- return value === 'wait' || value === 'steer' ? value : undefined;
65
- }
66
79
  function normalizeBlocks(value) {
67
80
  return Array.isArray(value) ? value.filter(isRecord).map((block) => ({ ...block })) : [];
68
81
  }
69
- function normalizeStringArray(value) {
70
- return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
71
- }
82
+ // ── Boundary schemas ─────────────────────────────────────────────────────────
83
+ //
84
+ // Canonical wire schemas made tolerant of partial rows (older engines and
85
+ // trimmed payloads omit fields); every field that is present is validated
86
+ // against the canonical contract, and enum fields degrade to the relay
87
+ // fallback instead of failing the whole payload.
88
+ const WireAgentChannelSchema = AgentSchema.shape.channels
89
+ .unwrap()
90
+ .element.partial()
91
+ .extend({ role: ChannelMemberInfoSchema.shape.role.optional().catch(undefined) });
92
+ const WireAgentSchema = AgentSchema.omit({ channels: true })
93
+ .partial()
94
+ .extend({
95
+ type: AgentTypeSchema.optional().catch(undefined),
96
+ status: AgentStatusSchema.optional().catch(undefined),
97
+ metadata: AgentSchema.shape.metadata.nullable().optional(),
98
+ channels: z.array(z.unknown()).optional(),
99
+ });
100
+ const WireRegistrationSchema = CreateAgentResponseSchema.partial().extend({
101
+ status: AgentStatusSchema.optional().catch(undefined),
102
+ });
103
+ const WirePresenceSchema = AgentPresenceInfoSchema.partial().extend({
104
+ status: AgentPresenceInfoSchema.shape.status.optional().catch(undefined),
105
+ });
106
+ const WireChannelMemberSchema = ChannelMemberInfoSchema.partial().extend({
107
+ role: ChannelMemberInfoSchema.shape.role.optional().catch(undefined),
108
+ });
109
+ const WireChannelSchema = ChannelSchema.omit({ members: true })
110
+ .partial()
111
+ .extend({
112
+ metadata: ChannelSchema.shape.metadata.unwrap().nullable().optional(),
113
+ members: z.array(z.unknown()).optional(),
114
+ });
115
+ // Accepts the union of canonical message rows: `MessageWithMeta` (channel
116
+ // listings), `Message` (raw rows, where the text is `body`), the core message
117
+ // payload carried by WebSocket events, and the nullable-sender message
118
+ // embedded in delivery ledger rows.
119
+ const WireMessageSchema = MessageWithMetaSchema.omit({
120
+ agent_id: true,
121
+ agent_name: true,
122
+ attachments: true,
123
+ blocks: true,
124
+ has_attachments: true,
125
+ injection_mode: true,
126
+ reactions: true,
127
+ })
128
+ .partial()
129
+ .extend({
130
+ agent_id: DeliveryMessageSchema.shape.agent_id.optional(),
131
+ agent_name: DeliveryMessageSchema.shape.agent_name.optional(),
132
+ body: MessageSchema.shape.body.optional(),
133
+ updated_at: MessageSchema.shape.updated_at.optional(),
134
+ channel_name: z.string().optional(),
135
+ conversation_id: z.string().optional(),
136
+ parent_id: z.string().optional(),
137
+ mode: MessageInjectionModeSchema.optional().catch(undefined),
138
+ injection_mode: MessageInjectionModeSchema.optional().catch(undefined),
139
+ metadata: MessageWithMetaSchema.shape.metadata.unwrap().nullable().optional(),
140
+ // Legacy alias for `metadata` on older message rows.
141
+ data: z.record(z.string(), z.unknown()).nullable().optional(),
142
+ attachments: z.array(z.unknown()).optional(),
143
+ blocks: z.unknown().optional(),
144
+ reactions: z.array(z.unknown()).optional(),
145
+ });
146
+ const WireInboxChannelSchema = InboxResponseSchema.shape.unread_channels.element.partial();
147
+ const WireInboxReactionSchema = InboxResponseSchema.shape.recent_reactions.element.partial();
148
+ const WireInboxDmSchema = InboxResponseSchema.shape.unread_dms.element
149
+ .omit({ last_message: true })
150
+ .partial()
151
+ .extend({ last_message: z.unknown().optional() });
152
+ const WireInboxLastMessageSchema = InboxResponseSchema.shape.unread_dms.element.shape.last_message
153
+ .unwrap()
154
+ .partial();
155
+ const WireInboxSchema = z.object({
156
+ unread_channels: z.array(WireInboxChannelSchema).optional(),
157
+ mentions: z.array(z.unknown()).optional(),
158
+ unread_dms: z.array(z.unknown()).optional(),
159
+ recent_reactions: z.array(z.unknown()).optional(),
160
+ });
161
+ const WireReadReceiptSchema = ReadReceiptSchema.extend(ReaderInfoSchema.shape).partial();
162
+ const WireChannelReadStatusSchema = ChannelReadStatusSchema.partial();
163
+ const WireSearchResultSchema = SearchMessageResultSchema.partial();
164
+ // Delivery statuses reported by the legacy `api.relaycast.dev` engine
165
+ // (the `@relaycast/types` 3.x lifecycle names not carried into 6.x).
166
+ const LegacyDeliveryStatusSchema = z.enum(['accepted', 'deferred']);
167
+ // A delivery ledger row, optionally carrying its embedded message payload
168
+ // (`DeliveryItem`) — transitions return the bare row.
169
+ const WireDeliverySchema = DeliverySchema.partial().extend({
170
+ status: z.union([DeliveryStatusSchema, LegacyDeliveryStatusSchema]).optional().catch(undefined),
171
+ message: DeliveryMessageSchema.partial().nullable().optional(),
172
+ });
173
+ const WireGroupDmSchema = CreateGroupDmResponseSchema.omit({ dm_type: true, participants: true })
174
+ .partial()
175
+ .extend({
176
+ conversation_id: z.string().optional(),
177
+ participants: z.array(z.unknown()).optional(),
178
+ });
179
+ // ── Normalizers ──────────────────────────────────────────────────────────────
72
180
  export function normalizeAgentChannel(input) {
73
- const record = isRecord(input) ? input : {};
74
- const name = normalizeOptionalChannelName(readString(record, 'name', 'channelName', 'channel_name')) ?? '';
75
- return {
76
- id: readString(record, 'id', 'channelId', 'channel_id') ?? name,
181
+ const channel = parseWire(WireAgentChannelSchema, input);
182
+ const name = normalizeOptionalChannelName(channel.name) ?? '';
183
+ return compact({
184
+ id: channel.id ?? name,
77
185
  name,
78
- role: normalizeRole(readString(record, 'role')),
79
- ...(readString(record, 'joinedAt', 'joined_at')
80
- ? { joinedAt: readString(record, 'joinedAt', 'joined_at') }
81
- : {}),
82
- };
186
+ role: channel.role ?? 'member',
187
+ joinedAt: opt(channel.joined_at),
188
+ });
83
189
  }
84
190
  export function normalizeAgent(input) {
85
- const record = isRecord(input) ? input : {};
86
- const id = readString(record, 'id', 'agentId', 'agent_id') ??
87
- readString(record, 'name', 'agentName', 'agent_name') ??
88
- '';
89
- const name = readString(record, 'name', 'agentName', 'agent_name') ?? id;
90
- const lastSeenAt = readString(record, 'lastSeenAt', 'last_seen', 'lastSeen');
91
- const createdAt = readString(record, 'createdAt', 'created_at');
92
- const persona = readNullableString(record, 'persona');
93
- return {
191
+ const agent = parseWire(WireAgentSchema, input);
192
+ const id = agent.id ?? agent.name ?? '';
193
+ return compact({
94
194
  id,
95
- name,
96
- type: normalizeAgentType(readString(record, 'type')),
97
- status: normalizeAgentStatus(readString(record, 'status')),
98
- ...(persona ? { persona } : {}),
99
- metadata: readRecord(record, 'metadata') ?? {},
100
- ...(lastSeenAt ? { lastSeenAt } : {}),
101
- ...(createdAt ? { createdAt } : {}),
102
- channels: readArray(record, 'channels').map(normalizeAgentChannel),
103
- };
195
+ name: agent.name ?? id,
196
+ type: agent.type ?? 'agent',
197
+ status: agent.status ?? 'unknown',
198
+ persona: opt(agent.persona),
199
+ metadata: agent.metadata ?? {},
200
+ lastSeenAt: opt(agent.last_seen),
201
+ createdAt: opt(agent.created_at),
202
+ channels: (agent.channels ?? []).map(normalizeAgentChannel),
203
+ });
104
204
  }
105
205
  export function normalizeAgentRegistration(input) {
106
- const record = isRecord(input) ? input : {};
107
- const id = readString(record, 'id', 'agentId', 'agent_id') ?? readString(record, 'name') ?? '';
108
- const name = readString(record, 'name', 'agentName', 'agent_name') ?? id;
109
- const createdAt = readString(record, 'createdAt', 'created_at');
110
- return {
206
+ const registration = parseWire(WireRegistrationSchema, input);
207
+ const id = registration.id ?? registration.name ?? '';
208
+ return compact({
111
209
  id,
112
- name,
113
- token: readString(record, 'token') ?? '',
114
- status: normalizeAgentStatus(readString(record, 'status')),
115
- ...(createdAt ? { createdAt } : {}),
116
- };
210
+ name: registration.name ?? id,
211
+ token: registration.token ?? '',
212
+ status: registration.status ?? 'unknown',
213
+ createdAt: opt(registration.created_at),
214
+ });
117
215
  }
118
216
  export function normalizeAgentPresence(input) {
119
- const record = isRecord(input) ? input : {};
120
- const agentId = readString(record, 'agentId', 'agent_id', 'id') ?? '';
121
- const agentName = readString(record, 'agentName', 'agent_name', 'name') ?? agentId;
217
+ const presence = parseWire(WirePresenceSchema, input);
218
+ const agentId = presence.agent_id ?? '';
122
219
  return {
123
220
  agentId,
124
- agentName,
125
- status: normalizeOnlineStatus(readString(record, 'status')),
221
+ agentName: presence.agent_name ?? agentId,
222
+ status: presence.status === 'online' ? 'online' : 'offline',
126
223
  };
127
224
  }
128
225
  export function normalizeChannelMember(input) {
129
- const record = isRecord(input) ? input : {};
130
- const agentId = readString(record, 'agentId', 'agent_id', 'id') ?? '';
131
- const agentName = readString(record, 'agentName', 'agent_name', 'name') ?? agentId;
132
- return {
226
+ const member = parseWire(WireChannelMemberSchema, input);
227
+ const agentId = member.agent_id ?? '';
228
+ return compact({
133
229
  agentId,
134
- agentName,
135
- role: normalizeRole(readString(record, 'role')),
136
- ...(readString(record, 'joinedAt', 'joined_at')
137
- ? { joinedAt: readString(record, 'joinedAt', 'joined_at') }
138
- : {}),
139
- muted: readBoolean(record, 'muted', 'isMuted', 'is_muted') ?? false,
140
- };
230
+ agentName: member.agent_name ?? agentId,
231
+ role: member.role ?? 'member',
232
+ joinedAt: opt(member.joined_at),
233
+ muted: member.is_muted ?? false,
234
+ });
141
235
  }
142
236
  export function normalizeChannel(input) {
143
- const record = isRecord(input) ? input : {};
144
- const name = normalizeOptionalChannelName(readString(record, 'name', 'channelName', 'channel_name')) ?? '';
145
- const topic = readNullableString(record, 'topic');
146
- const createdBy = readString(record, 'createdBy', 'created_by');
147
- const createdAt = readString(record, 'createdAt', 'created_at');
148
- const memberCount = readNumber(record, 'memberCount', 'member_count');
149
- return {
150
- id: readString(record, 'id', 'channelId', 'channel_id') ?? name,
237
+ const channel = parseWire(WireChannelSchema, input);
238
+ const name = normalizeOptionalChannelName(channel.name) ?? '';
239
+ return compact({
240
+ id: channel.id ?? name,
151
241
  name,
152
- ...(topic ? { topic } : {}),
153
- metadata: readRecord(record, 'metadata') ?? {},
154
- ...(createdBy ? { createdBy } : {}),
155
- ...(createdAt ? { createdAt } : {}),
156
- archived: readBoolean(record, 'archived', 'isArchived', 'is_archived') ?? false,
157
- ...(memberCount !== undefined ? { memberCount } : {}),
158
- members: readArray(record, 'members').map(normalizeChannelMember),
159
- };
242
+ topic: opt(channel.topic),
243
+ metadata: channel.metadata ?? {},
244
+ createdBy: opt(channel.created_by),
245
+ createdAt: opt(channel.created_at),
246
+ archived: channel.is_archived ?? false,
247
+ memberCount: channel.member_count,
248
+ members: (channel.members ?? []).map(normalizeChannelMember),
249
+ });
160
250
  }
161
251
  export function normalizeAttachment(input) {
162
- const record = isRecord(input) ? input : {};
163
- const type = readString(record, 'type');
164
- if (type === 'text') {
165
- return {
166
- type,
167
- text: readString(record, 'text', 'content') ?? '',
168
- ...(readString(record, 'label') ? { label: readString(record, 'label') } : {}),
169
- };
170
- }
171
- if (type === 'image') {
172
- return {
173
- type,
174
- ...(readString(record, 'url') ? { url: readString(record, 'url') } : {}),
175
- ...(readString(record, 'data') ? { data: readString(record, 'data') } : {}),
176
- ...(readString(record, 'mimeType', 'mime_type')
177
- ? { mimeType: readString(record, 'mimeType', 'mime_type') }
178
- : {}),
179
- ...(readString(record, 'alt') ? { alt: readString(record, 'alt') } : {}),
180
- ...(readString(record, 'label') ? { label: readString(record, 'label') } : {}),
181
- };
182
- }
183
- if (type === 'link') {
184
- return {
185
- type,
186
- url: readString(record, 'url') ?? '',
187
- ...(readString(record, 'title') ? { title: readString(record, 'title') } : {}),
188
- ...(readString(record, 'label') ? { label: readString(record, 'label') } : {}),
189
- };
190
- }
191
- if (type === 'file') {
192
- return {
193
- type,
194
- path: readString(record, 'path') ?? readString(record, 'filename', 'name') ?? '',
195
- ...(readNumber(record, 'line') !== undefined ? { line: readNumber(record, 'line') } : {}),
196
- ...(readString(record, 'label') ? { label: readString(record, 'label') } : {}),
197
- };
198
- }
199
- if (type === 'json') {
200
- return {
201
- type,
202
- value: readValue(record, 'value'),
203
- ...(readString(record, 'label') ? { label: readString(record, 'label') } : {}),
204
- };
205
- }
206
- if (type === 'diff') {
207
- return {
208
- type,
209
- patch: readString(record, 'patch', 'diff') ?? '',
210
- ...(readString(record, 'label') ? { label: readString(record, 'label') } : {}),
211
- };
212
- }
213
- if (type === 'artifact') {
214
- return {
215
- type,
216
- id: readString(record, 'id', 'artifactId', 'artifact_id') ?? '',
217
- ...(readString(record, 'url') ? { url: readString(record, 'url') } : {}),
218
- ...(readString(record, 'label') ? { label: readString(record, 'label') } : {}),
219
- };
252
+ const wired = toWire(input);
253
+ const record = isRecord(wired) ? wired : {};
254
+ const type = str(record, 'type');
255
+ const label = opt(str(record, 'label'));
256
+ switch (type) {
257
+ case 'text':
258
+ return compact({ type, text: str(record, 'text') ?? str(record, 'content') ?? '', label });
259
+ case 'image':
260
+ return compact({
261
+ type,
262
+ url: opt(str(record, 'url')),
263
+ data: opt(str(record, 'data')),
264
+ mimeType: opt(str(record, 'mime_type')),
265
+ alt: opt(str(record, 'alt')),
266
+ label,
267
+ });
268
+ case 'link':
269
+ return compact({ type, url: str(record, 'url') ?? '', title: opt(str(record, 'title')), label });
270
+ case 'file':
271
+ return compact({
272
+ type,
273
+ path: str(record, 'path') ?? str(record, 'filename') ?? '',
274
+ line: num(record, 'line'),
275
+ label,
276
+ });
277
+ case 'json':
278
+ return compact({ type, value: record.value, label });
279
+ case 'diff':
280
+ return compact({ type, patch: str(record, 'patch') ?? str(record, 'diff') ?? '', label });
281
+ case 'artifact':
282
+ return compact({
283
+ type,
284
+ id: str(record, 'id') ?? str(record, 'artifact_id') ?? '',
285
+ url: opt(str(record, 'url')),
286
+ label,
287
+ });
288
+ default: {
289
+ // Canonical stored file attachment (`FileAttachment`).
290
+ const filename = str(record, 'filename');
291
+ return compact({
292
+ id: str(record, 'id') ?? str(record, 'file_id') ?? filename ?? '',
293
+ filename: opt(filename),
294
+ contentType: opt(str(record, 'content_type')),
295
+ sizeBytes: num(record, 'size_bytes'),
296
+ });
297
+ }
220
298
  }
221
- const filename = readString(record, 'filename', 'name');
222
- const contentType = readString(record, 'contentType', 'content_type');
223
- const sizeBytes = readNumber(record, 'sizeBytes', 'size_bytes');
224
- return {
225
- id: readString(record, 'id', 'fileId', 'file_id') ?? filename ?? '',
226
- ...(filename ? { filename } : {}),
227
- ...(contentType ? { contentType } : {}),
228
- ...(sizeBytes !== undefined ? { sizeBytes } : {}),
229
- };
230
299
  }
231
300
  export function normalizeReaction(input) {
232
- const record = isRecord(input) ? input : {};
301
+ const reaction = parseWire(ReactionGroupSchema.partial(), input);
233
302
  return {
234
- emoji: readString(record, 'emoji') ?? '',
235
- count: readNumber(record, 'count') ?? 0,
236
- agents: normalizeStringArray(readValue(record, 'agents', 'agentNames', 'agent_names')),
303
+ emoji: reaction.emoji ?? '',
304
+ count: reaction.count ?? 0,
305
+ agents: reaction.agents ?? [],
237
306
  };
238
307
  }
239
308
  export function normalizeMessage(input, context = {}) {
240
- const record = isRecord(input) ? input : {};
241
- const fromRecord = isRecord(readValue(record, 'from', 'sender', 'agent'))
242
- ? readValue(record, 'from', 'sender', 'agent')
243
- : undefined;
244
- const channelName = context.channelName ??
245
- normalizeOptionalChannelName(readString(record, 'channelName', 'channel_name', 'channel'));
246
- const channelId = context.channelId ?? readString(record, 'channelId', 'channel_id');
247
- const conversationId = context.conversationId ?? readString(record, 'conversationId', 'conversation_id');
248
- const parentId = context.parentId ?? readString(record, 'parentId', 'parent_id');
249
- const threadId = context.threadId ?? readNullableString(record, 'threadId', 'thread_id');
250
- const createdAt = readString(record, 'createdAt', 'created_at') ?? context.createdAt;
251
- const updatedAt = readNullableString(record, 'updatedAt', 'updated_at');
252
- const metadata = readRecord(record, 'metadata') ?? readRecord(record, 'data');
253
- const replyCount = readNumber(record, 'replyCount', 'reply_count');
254
- const readByCount = readNumber(record, 'readByCount', 'read_by_count');
309
+ const message = parseWire(WireMessageSchema, input);
310
+ const channelName = context.channelName ?? normalizeOptionalChannelName(message.channel_name);
311
+ const channelId = context.channelId ?? message.channel_id;
312
+ const conversationId = context.conversationId ?? message.conversation_id;
313
+ const parentId = context.parentId ?? message.parent_id;
255
314
  const kind = context.kind ??
256
315
  (parentId ? 'thread_reply' : conversationId ? 'dm' : channelId || channelName ? 'channel' : 'unknown');
257
- const id = readString(record, 'id', 'messageId', 'message_id') ?? '';
258
- return {
316
+ const id = message.id ?? '';
317
+ return compact({
259
318
  id,
260
319
  messageId: id,
261
320
  kind,
262
- text: readString(record, 'text', 'body') ?? '',
263
- from: {
264
- ...((readString(record, 'agentId', 'agent_id', 'fromId', 'from_id') ??
265
- readString(fromRecord, 'id', 'agentId', 'agent_id'))
266
- ? {
267
- id: readString(record, 'agentId', 'agent_id', 'fromId', 'from_id') ??
268
- readString(fromRecord, 'id', 'agentId', 'agent_id'),
269
- }
270
- : {}),
271
- ...((readString(record, 'agentName', 'agent_name', 'fromName', 'from_name', 'from') ??
272
- readString(fromRecord, 'name', 'agentName', 'agent_name'))
273
- ? {
274
- name: readString(record, 'agentName', 'agent_name', 'fromName', 'from_name', 'from') ??
275
- readString(fromRecord, 'name', 'agentName', 'agent_name'),
276
- }
277
- : {}),
278
- },
279
- ...(channelId || channelName
280
- ? {
281
- channel: {
282
- ...(channelId ? { id: channelId } : {}),
283
- ...(channelName ? { name: channelName } : {}),
284
- },
285
- }
286
- : {}),
287
- ...(conversationId ? { conversationId } : {}),
288
- ...(threadId ? { threadId } : {}),
289
- ...(parentId ? { parentId } : {}),
290
- ...(normalizeMode(readString(record, 'mode', 'injectionMode', 'injection_mode'))
291
- ? {
292
- mode: normalizeMode(readString(record, 'mode', 'injectionMode', 'injection_mode')),
293
- }
294
- : {}),
295
- ...(createdAt ? { createdAt } : {}),
296
- ...(updatedAt ? { updatedAt } : {}),
297
- ...(metadata ? { metadata } : {}),
298
- blocks: normalizeBlocks(readValue(record, 'blocks')),
299
- attachments: readArray(record, 'attachments').map(normalizeAttachment),
300
- ...(replyCount !== undefined ? { replyCount } : {}),
301
- reactions: readArray(record, 'reactions').map(normalizeReaction),
302
- ...(readByCount !== undefined ? { readByCount } : {}),
303
- mentions: normalizeStringArray(readValue(record, 'mentions')),
304
- };
321
+ text: message.text ?? message.body ?? '',
322
+ from: compact({ id: opt(message.agent_id), name: opt(message.agent_name) }),
323
+ channel: channelId || channelName
324
+ ? compact({ id: opt(channelId), name: opt(channelName) })
325
+ : undefined,
326
+ conversationId: opt(conversationId),
327
+ threadId: opt(context.threadId ?? message.thread_id),
328
+ parentId: opt(parentId),
329
+ mode: message.mode ?? message.injection_mode,
330
+ createdAt: opt(message.created_at ?? context.createdAt),
331
+ updatedAt: opt(message.updated_at),
332
+ metadata: message.metadata ?? message.data ?? undefined,
333
+ blocks: normalizeBlocks(message.blocks),
334
+ attachments: (message.attachments ?? []).map(normalizeAttachment),
335
+ replyCount: message.reply_count,
336
+ reactions: (message.reactions ?? []).map(normalizeReaction),
337
+ readByCount: message.read_by_count,
338
+ mentions: message.mentions ?? [],
339
+ });
305
340
  }
306
341
  export function normalizeThread(input) {
307
342
  const record = isRecord(input) ? input : {};
308
- const parent = normalizeMessage(readValue(record, 'parent'), { kind: 'channel' });
309
- const replies = readArray(record, 'replies').map((reply) => normalizeMessage(reply, {
310
- kind: 'thread_reply',
311
- channelId: parent.channel?.id,
312
- channelName: parent.channel?.name,
313
- threadId: parent.threadId ?? parent.id,
314
- parentId: parent.id,
315
- }));
316
- return { parent, replies };
343
+ const parent = normalizeMessage(record.parent, { kind: 'channel' });
344
+ const replies = Array.isArray(record.replies) ? record.replies : [];
345
+ return {
346
+ parent,
347
+ replies: replies.map((reply) => normalizeMessage(reply, {
348
+ kind: 'thread_reply',
349
+ channelId: parent.channel?.id,
350
+ channelName: parent.channel?.name,
351
+ threadId: parent.threadId ?? parent.id,
352
+ parentId: parent.id,
353
+ })),
354
+ };
317
355
  }
318
356
  function normalizeInboxLastMessage(input) {
319
- const record = isRecord(input) ? input : undefined;
320
- if (!record)
357
+ if (!isRecord(input))
321
358
  return undefined;
322
- const createdAt = readString(record, 'createdAt', 'created_at');
323
- return {
324
- id: readString(record, 'id', 'messageId', 'message_id') ?? '',
325
- text: readString(record, 'text', 'body') ?? '',
326
- ...(createdAt ? { createdAt } : {}),
327
- };
359
+ const last = parseWire(WireInboxLastMessageSchema, input);
360
+ return compact({
361
+ id: last.id ?? '',
362
+ text: last.text ?? '',
363
+ createdAt: opt(last.created_at),
364
+ });
328
365
  }
329
366
  function normalizeInboxDirect(input) {
330
- const record = isRecord(input) ? input : {};
331
- const lastMessage = normalizeInboxLastMessage(readValue(record, 'lastMessage', 'last_message'));
332
- return {
333
- conversationId: readString(record, 'conversationId', 'conversation_id') ?? '',
334
- from: readString(record, 'from', 'agentName', 'agent_name') ?? '',
335
- unreadCount: readNumber(record, 'unreadCount', 'unread_count') ?? 0,
336
- ...(lastMessage ? { lastMessage } : {}),
337
- };
367
+ const dm = parseWire(WireInboxDmSchema, input);
368
+ return compact({
369
+ conversationId: dm.conversation_id ?? '',
370
+ from: dm.from ?? '',
371
+ unreadCount: dm.unread_count ?? 0,
372
+ lastMessage: normalizeInboxLastMessage(dm.last_message),
373
+ });
338
374
  }
339
375
  function normalizeInboxReaction(input) {
340
- const record = isRecord(input) ? input : {};
341
- const createdAt = readString(record, 'createdAt', 'created_at');
342
- return {
343
- messageId: readString(record, 'messageId', 'message_id') ?? '',
344
- channelName: normalizeOptionalChannelName(readString(record, 'channelName', 'channel_name', 'channel')) ?? '',
345
- emoji: readString(record, 'emoji') ?? '',
346
- agentName: readString(record, 'agentName', 'agent_name') ?? '',
347
- ...(createdAt ? { createdAt } : {}),
348
- };
376
+ const reaction = parseWire(WireInboxReactionSchema, input);
377
+ return compact({
378
+ messageId: reaction.message_id ?? '',
379
+ channelName: normalizeOptionalChannelName(reaction.channel_name) ?? '',
380
+ emoji: reaction.emoji ?? '',
381
+ agentName: reaction.agent_name ?? '',
382
+ createdAt: opt(reaction.created_at),
383
+ });
349
384
  }
350
385
  export function normalizeInbox(input) {
351
- const record = isRecord(input) ? input : {};
386
+ const inbox = parseWire(WireInboxSchema, input);
352
387
  return {
353
- unreadChannels: readArray(record, 'unreadChannels', 'unread_channels').map((item) => {
354
- const itemRecord = isRecord(item) ? item : {};
355
- return {
356
- channelName: normalizeOptionalChannelName(readString(itemRecord, 'channelName', 'channel_name', 'channel')) ??
357
- '',
358
- unreadCount: readNumber(itemRecord, 'unreadCount', 'unread_count') ?? 0,
359
- };
360
- }),
361
- mentions: readArray(record, 'mentions').map((item) => {
362
- const itemRecord = isRecord(item) ? item : {};
363
- return normalizeMessage(itemRecord, {
364
- kind: 'channel',
365
- channelName: normalizeOptionalChannelName(readString(itemRecord, 'channelName', 'channel_name', 'channel')),
366
- });
367
- }),
368
- unreadDms: readArray(record, 'unreadDms', 'unread_dms').map(normalizeInboxDirect),
369
- recentReactions: readArray(record, 'recentReactions', 'recent_reactions').map(normalizeInboxReaction),
388
+ unreadChannels: (inbox.unread_channels ?? []).map((channel) => ({
389
+ channelName: normalizeOptionalChannelName(channel.channel_name) ?? '',
390
+ unreadCount: channel.unread_count ?? 0,
391
+ })),
392
+ mentions: (inbox.mentions ?? []).map((mention) => normalizeMessage(mention, { kind: 'channel' })),
393
+ unreadDms: (inbox.unread_dms ?? []).map(normalizeInboxDirect),
394
+ recentReactions: (inbox.recent_reactions ?? []).map(normalizeInboxReaction),
370
395
  };
371
396
  }
372
397
  export function normalizeReadReceipt(input) {
373
- const record = isRecord(input) ? input : {};
374
- const readAt = readString(record, 'readAt', 'read_at');
375
- return {
376
- messageId: readString(record, 'messageId', 'message_id') ?? '',
377
- ...(readString(record, 'agentId', 'agent_id')
378
- ? { agentId: readString(record, 'agentId', 'agent_id') }
379
- : {}),
380
- ...(readString(record, 'agentName', 'agent_name')
381
- ? { agentName: readString(record, 'agentName', 'agent_name') }
382
- : {}),
383
- ...(readAt ? { readAt } : {}),
384
- };
398
+ const receipt = parseWire(WireReadReceiptSchema, input);
399
+ return compact({
400
+ messageId: receipt.message_id ?? '',
401
+ agentId: opt(receipt.agent_id),
402
+ agentName: opt(receipt.agent_name),
403
+ readAt: opt(receipt.read_at),
404
+ });
385
405
  }
386
406
  export function normalizeChannelReadStatus(input) {
387
- const record = isRecord(input) ? input : {};
388
- const lastReadId = readNullableString(record, 'lastReadId', 'last_read_id');
389
- const lastReadAt = readNullableString(record, 'lastReadAt', 'last_read_at');
390
- return {
391
- agentName: readString(record, 'agentName', 'agent_name') ?? '',
392
- ...(lastReadId ? { lastReadId } : {}),
393
- ...(lastReadAt ? { lastReadAt } : {}),
394
- };
395
- }
396
- // Relaycast durable delivery ledger statuses mapped onto relay inbox states.
397
- // `accepted` means queued for the recipient; the ledger has no `read` state.
407
+ const status = parseWire(WireChannelReadStatusSchema, input);
408
+ return compact({
409
+ agentName: status.agent_name ?? '',
410
+ lastReadId: opt(status.last_read_id),
411
+ lastReadAt: opt(status.last_read_at),
412
+ });
413
+ }
414
+ // Canonical durable delivery statuses mapped onto relay inbox states:
415
+ // the terminal `acked` surfaces as `read` and `dead_lettered` as `failed`;
416
+ // a deferred 6.x row stays `queued` with a future `available_at`. Typed
417
+ // against `DeliveryStatus` so a new canonical status fails to compile here
418
+ // until it is mapped. Legacy `api.relaycast.dev` statuses map alongside.
398
419
  const INBOX_STATE_BY_DELIVERY_STATUS = {
399
- accepted: 'queued',
420
+ queued: 'queued',
400
421
  delivered: 'delivered',
401
- deferred: 'deferred',
422
+ acked: 'read',
402
423
  failed: 'failed',
424
+ dead_lettered: 'failed',
425
+ // Legacy 3.x lifecycle names.
426
+ accepted: 'queued',
427
+ deferred: 'deferred',
403
428
  };
404
429
  export function normalizeInboxItemState(value) {
405
- return (value !== undefined ? INBOX_STATE_BY_DELIVERY_STATUS[value] : undefined) ?? 'queued';
406
- }
407
- function normalizeDeliveryMetadata(record) {
408
- const metadata = {};
409
- const mode = readString(record, 'mode');
410
- const reason = readNullableString(record, 'reason');
411
- const priority = readString(record, 'priority');
412
- const retryable = readBoolean(record, 'retryable');
413
- const error = readNullableString(record, 'error');
414
- const deadline = readNullableString(record, 'deadline');
415
- if (mode)
416
- metadata.mode = mode;
417
- if (reason)
418
- metadata.reason = reason;
419
- if (priority)
420
- metadata.priority = priority;
421
- if (retryable !== undefined)
422
- metadata.retryable = retryable;
423
- if (error)
424
- metadata.error = error;
425
- if (deadline)
426
- metadata.deadline = deadline;
430
+ const status = z.union([DeliveryStatusSchema, LegacyDeliveryStatusSchema]).safeParse(value);
431
+ return status.success ? INBOX_STATE_BY_DELIVERY_STATUS[status.data] : 'queued';
432
+ }
433
+ function normalizeDeliveryMetadata(delivery) {
434
+ const metadata = compact({
435
+ mode: opt(delivery.mode),
436
+ reason: opt(delivery.reason),
437
+ priority: opt(delivery.priority),
438
+ retryable: delivery.retryable ?? undefined,
439
+ error: opt(delivery.error),
440
+ deadline: opt(delivery.deadline),
441
+ });
427
442
  return Object.keys(metadata).length > 0 ? metadata : undefined;
428
443
  }
429
444
  /**
@@ -431,107 +446,92 @@ function normalizeDeliveryMetadata(record) {
431
446
  * its embedded message payload) into a relay `InboxItem`.
432
447
  */
433
448
  export function normalizeInboxItem(input, context = {}) {
434
- const record = isRecord(input) ? input : {};
435
- const messageId = readString(record, 'messageId', 'message_id');
436
- const channelId = readString(record, 'channelId', 'channel_id');
437
- const agentId = readString(record, 'agentId', 'agent_id');
438
- const availableAt = readNullableString(record, 'availableAt', 'available_at');
439
- const messageRecord = readRecord(record, 'message');
440
- const metadata = normalizeDeliveryMetadata(record);
441
- return {
442
- id: readString(record, 'id', 'deliveryId', 'delivery_id') ?? '',
443
- recipient: {
444
- name: context.recipientName ?? agentId ?? '',
445
- ...(agentId ? { id: agentId } : {}),
446
- },
447
- state: normalizeInboxItemState(readString(record, 'status')),
449
+ const delivery = parseWire(WireDeliverySchema, input);
450
+ return compact({
451
+ id: delivery.id ?? '',
452
+ recipient: compact({
453
+ name: context.recipientName ?? delivery.agent_id ?? '',
454
+ id: opt(delivery.agent_id),
455
+ }),
456
+ state: delivery.status ? INBOX_STATE_BY_DELIVERY_STATUS[delivery.status] : 'queued',
448
457
  // The relaycast ledger does not expose attempt counts.
449
458
  attempts: 0,
450
- ...(availableAt ? { availableAt } : {}),
451
- message: normalizeMessage(messageRecord ?? {
452
- ...(messageId ? { id: messageId } : {}),
453
- ...(channelId ? { channel_id: channelId } : {}),
454
- }),
455
- ...(metadata ? { metadata } : {}),
456
- };
459
+ availableAt: opt(delivery.available_at),
460
+ message: normalizeMessage(delivery.message ??
461
+ compact({ id: opt(delivery.message_id), channel_id: opt(delivery.channel_id) })),
462
+ metadata: normalizeDeliveryMetadata(delivery),
463
+ });
457
464
  }
458
465
  /**
459
466
  * Normalize the delivery row returned by a relaycast ack/fail/defer transition
460
467
  * into the relay delivery result contract.
461
468
  */
462
469
  export function normalizeDeliveryTransition(action, input) {
463
- const record = isRecord(input) ? input : {};
464
- const availableAt = readNullableString(record, 'availableAt', 'available_at');
465
- return {
470
+ const delivery = parseWire(WireDeliverySchema, input);
471
+ return compact({
466
472
  supported: true,
467
473
  action,
468
- deliveryId: readString(record, 'id', 'deliveryId', 'delivery_id') ?? '',
469
- messageId: readString(record, 'messageId', 'message_id') ?? '',
470
- state: normalizeInboxItemState(readString(record, 'status')),
471
- ...(action === 'defer' && availableAt ? { deferUntil: availableAt } : {}),
472
- };
474
+ deliveryId: delivery.id ?? '',
475
+ messageId: delivery.message_id ?? '',
476
+ state: delivery.status ? INBOX_STATE_BY_DELIVERY_STATUS[delivery.status] : 'queued',
477
+ deferUntil: action === 'defer' ? opt(delivery.available_at) : undefined,
478
+ });
473
479
  }
474
480
  export function normalizeSearchResult(input) {
475
- const record = isRecord(input) ? input : {};
476
- const createdAt = readString(record, 'createdAt', 'created_at');
477
- return {
478
- id: readString(record, 'id', 'messageId', 'message_id') ?? '',
479
- channelName: normalizeOptionalChannelName(readString(record, 'channelName', 'channel_name', 'channel')) ?? '',
480
- agentName: readString(record, 'agentName', 'agent_name') ?? '',
481
- text: readString(record, 'text', 'body') ?? '',
482
- ...(createdAt ? { createdAt } : {}),
483
- relevanceScore: readNumber(record, 'relevanceScore', 'relevance_score') ?? 0,
484
- };
481
+ const result = parseWire(WireSearchResultSchema, input);
482
+ return compact({
483
+ id: result.id ?? '',
484
+ channelName: normalizeOptionalChannelName(result.channel_name) ?? '',
485
+ agentName: result.agent_name ?? '',
486
+ text: result.text ?? '',
487
+ createdAt: opt(result.created_at),
488
+ relevanceScore: result.relevance_score ?? 0,
489
+ });
485
490
  }
486
491
  export function normalizeGroupDirectConversation(input) {
487
- const record = isRecord(input) ? input : {};
488
- const name = readNullableString(record, 'name');
489
- const createdAt = readString(record, 'createdAt', 'created_at');
490
- return {
491
- id: readString(record, 'id', 'conversationId', 'conversation_id') ?? '',
492
- ...(readString(record, 'channelId', 'channel_id')
493
- ? { channelId: readString(record, 'channelId', 'channel_id') }
494
- : {}),
495
- ...(name ? { name } : {}),
496
- participants: readArray(record, 'participants')
497
- .map((participant) => {
498
- const participantRecord = isRecord(participant) ? participant : undefined;
499
- return typeof participant === 'string'
500
- ? participant
501
- : readString(participantRecord, 'agentName', 'agent_name', 'agentId', 'agent_id');
502
- })
492
+ const conversation = parseWire(WireGroupDmSchema, input);
493
+ return compact({
494
+ id: conversation.id ?? conversation.conversation_id ?? '',
495
+ channelId: opt(conversation.channel_id),
496
+ name: opt(conversation.name),
497
+ participants: (conversation.participants ?? [])
498
+ .map((participant) => typeof participant === 'string'
499
+ ? participant
500
+ : isRecord(participant)
501
+ ? (str(participant, 'agent_name') ?? str(participant, 'agent_id'))
502
+ : undefined)
503
503
  .filter((participant) => typeof participant === 'string'),
504
- ...(createdAt ? { createdAt } : {}),
505
- };
506
- }
504
+ createdAt: opt(conversation.created_at),
505
+ });
506
+ }
507
+ const MEMBERSHIP_EVENT_TYPES = {
508
+ 'member.joined': 'memberJoined',
509
+ 'member.left': 'memberLeft',
510
+ 'member.channel_muted': 'channelMuted',
511
+ 'member.channel_unmuted': 'channelUnmuted',
512
+ };
507
513
  export function normalizeMessagingEvent(input) {
508
- const record = isRecord(input) ? input : {};
509
- const sourceType = readString(record, 'type');
514
+ const wired = toWire(input);
515
+ const record = isRecord(wired) ? wired : {};
516
+ const sourceType = str(record, 'type');
510
517
  switch (sourceType) {
511
- case 'message.created': {
512
- const channel = normalizeOptionalChannelName(readString(record, 'channel')) ?? '';
513
- return {
514
- type: 'messageCreated',
515
- channel,
516
- message: normalizeMessage(readValue(record, 'message'), { kind: 'channel', channelName: channel }),
517
- };
518
- }
518
+ case 'message.created':
519
519
  case 'message.updated': {
520
- const channel = normalizeOptionalChannelName(readString(record, 'channel')) ?? '';
520
+ const channel = normalizeOptionalChannelName(str(record, 'channel')) ?? '';
521
521
  return {
522
- type: 'messageUpdated',
522
+ type: sourceType === 'message.created' ? 'messageCreated' : 'messageUpdated',
523
523
  channel,
524
- message: normalizeMessage(readValue(record, 'message'), { kind: 'channel', channelName: channel }),
524
+ message: normalizeMessage(record.message, { kind: 'channel', channelName: channel }),
525
525
  };
526
526
  }
527
527
  case 'thread.reply': {
528
- const channel = normalizeOptionalChannelName(readString(record, 'channel')) ?? '';
529
- const parentId = readString(record, 'parentId', 'parent_id') ?? '';
528
+ const channel = normalizeOptionalChannelName(str(record, 'channel')) ?? '';
529
+ const parentId = str(record, 'parent_id') ?? '';
530
530
  return {
531
531
  type: 'threadReply',
532
532
  channel,
533
533
  parentId,
534
- message: normalizeMessage(readValue(record, 'message'), {
534
+ message: normalizeMessage(record.message, {
535
535
  kind: 'thread_reply',
536
536
  channelName: channel,
537
537
  parentId,
@@ -539,130 +539,100 @@ export function normalizeMessagingEvent(input) {
539
539
  }),
540
540
  };
541
541
  }
542
- case 'dm.received': {
543
- const conversationId = readString(record, 'conversationId', 'conversation_id') ?? '';
544
- return {
545
- type: 'dmReceived',
546
- conversationId,
547
- message: normalizeMessage(readValue(record, 'message'), { kind: 'dm', conversationId }),
548
- };
549
- }
542
+ case 'dm.received':
550
543
  case 'group_dm.received': {
551
- const conversationId = readString(record, 'conversationId', 'conversation_id') ?? '';
544
+ const conversationId = str(record, 'conversation_id') ?? '';
545
+ const kind = sourceType === 'dm.received' ? 'dm' : 'group_dm';
552
546
  return {
553
- type: 'groupDmReceived',
547
+ type: kind === 'dm' ? 'dmReceived' : 'groupDmReceived',
554
548
  conversationId,
555
- message: normalizeMessage(readValue(record, 'message'), { kind: 'group_dm', conversationId }),
549
+ message: normalizeMessage(record.message, { kind, conversationId }),
556
550
  };
557
551
  }
552
+ // Legacy presence events emitted by older engines; current engines emit
553
+ // `agent.status.*` (surfaced as `unknown` events).
558
554
  case 'agent.online':
559
- return {
560
- type: 'agentOnline',
561
- agent: {
562
- name: readString(isRecord(readValue(record, 'agent'))
563
- ? readValue(record, 'agent')
564
- : undefined, 'name') ?? '',
565
- },
566
- };
567
555
  case 'agent.offline':
568
556
  return {
569
- type: 'agentOffline',
570
- agent: {
571
- name: readString(isRecord(readValue(record, 'agent'))
572
- ? readValue(record, 'agent')
573
- : undefined, 'name') ?? '',
574
- },
557
+ type: sourceType === 'agent.online' ? 'agentOnline' : 'agentOffline',
558
+ agent: { name: str(rec(record, 'agent'), 'name') ?? '' },
575
559
  };
576
560
  case 'agent.spawn_requested': {
577
- const agent = isRecord(readValue(record, 'agent')) ? readValue(record, 'agent') : {};
561
+ const agent = rec(record, 'agent');
578
562
  return {
579
563
  type: 'agentSpawnRequested',
580
- agent: {
581
- name: readString(agent, 'name') ?? '',
582
- ...(readString(agent, 'cli') ? { cli: readString(agent, 'cli') } : {}),
583
- ...(readString(agent, 'task') ? { task: readString(agent, 'task') } : {}),
584
- ...(normalizeOptionalChannelName(readNullableString(agent, 'channel') ?? undefined)
585
- ? { channel: normalizeOptionalChannelName(readNullableString(agent, 'channel') ?? undefined) }
586
- : {}),
587
- ...(readString(agent, 'model') ? { model: readString(agent, 'model') } : {}),
588
- alreadyExisted: readBoolean(agent, 'alreadyExisted', 'already_existed') ?? false,
589
- },
564
+ agent: compact({
565
+ name: str(agent, 'name') ?? '',
566
+ cli: opt(str(agent, 'cli')),
567
+ task: opt(str(agent, 'task')),
568
+ channel: normalizeOptionalChannelName(str(agent, 'channel')),
569
+ model: opt(str(agent, 'model')),
570
+ alreadyExisted: bool(agent, 'already_existed') ?? false,
571
+ }),
590
572
  };
591
573
  }
592
- case 'agent.release_requested': {
593
- const agent = isRecord(readValue(record, 'agent')) ? readValue(record, 'agent') : {};
594
- const reason = readNullableString(record, 'reason');
595
- return {
574
+ case 'agent.release_requested':
575
+ return compact({
596
576
  type: 'agentReleaseRequested',
597
- agent: { name: readString(agent, 'name') ?? '' },
598
- ...(reason ? { reason } : {}),
599
- deleted: readBoolean(record, 'deleted') ?? false,
600
- };
601
- }
577
+ agent: { name: str(rec(record, 'agent'), 'name') ?? '' },
578
+ reason: opt(str(record, 'reason')),
579
+ deleted: bool(record, 'deleted') ?? false,
580
+ });
602
581
  case 'channel.created':
603
582
  case 'channel.updated': {
604
- const channel = isRecord(readValue(record, 'channel'))
605
- ? readValue(record, 'channel')
606
- : {};
607
- const topic = readNullableString(channel, 'topic');
583
+ const channel = rec(record, 'channel');
608
584
  return {
609
585
  type: sourceType === 'channel.created' ? 'channelCreated' : 'channelUpdated',
610
- channel: {
611
- name: normalizeOptionalChannelName(readString(channel, 'name')) ?? '',
612
- ...(topic ? { topic } : {}),
613
- },
586
+ channel: compact({
587
+ name: normalizeOptionalChannelName(str(channel, 'name')) ?? '',
588
+ topic: opt(str(channel, 'topic')),
589
+ }),
614
590
  };
615
591
  }
616
- case 'channel.archived': {
617
- const channel = isRecord(readValue(record, 'channel'))
618
- ? readValue(record, 'channel')
619
- : {};
592
+ case 'channel.archived':
620
593
  return {
621
594
  type: 'channelArchived',
622
- channel: { name: normalizeOptionalChannelName(readString(channel, 'name')) ?? '' },
595
+ channel: { name: normalizeOptionalChannelName(str(rec(record, 'channel'), 'name')) ?? '' },
623
596
  };
624
- }
625
597
  case 'member.joined':
626
598
  case 'member.left':
627
599
  case 'member.channel_muted':
628
- case 'member.channel_unmuted': {
629
- const type = sourceType === 'member.joined'
630
- ? 'memberJoined'
631
- : sourceType === 'member.left'
632
- ? 'memberLeft'
633
- : sourceType === 'member.channel_muted'
634
- ? 'channelMuted'
635
- : 'channelUnmuted';
600
+ case 'member.channel_unmuted':
636
601
  return {
637
- type,
638
- channel: normalizeOptionalChannelName(readString(record, 'channel')) ?? '',
639
- agentName: readString(record, 'agentName', 'agent_name') ?? '',
602
+ type: MEMBERSHIP_EVENT_TYPES[sourceType],
603
+ channel: normalizeOptionalChannelName(str(record, 'channel')) ?? '',
604
+ agentName: str(record, 'agent_name') ?? '',
640
605
  };
641
- }
642
606
  case 'message.read':
643
- return {
607
+ return compact({
644
608
  type: 'messageRead',
645
- messageId: readString(record, 'messageId', 'message_id') ?? '',
646
- agentName: readString(record, 'agentName', 'agent_name') ?? '',
647
- ...(readString(record, 'readAt', 'read_at')
648
- ? { readAt: readString(record, 'readAt', 'read_at') }
649
- : {}),
650
- };
609
+ messageId: str(record, 'message_id') ?? '',
610
+ agentName: str(record, 'agent_name') ?? '',
611
+ readAt: opt(str(record, 'read_at')),
612
+ });
613
+ // Canonical reaction event plus the legacy split pair. The engine's raw
614
+ // workspace-stream frames and the durable event log (observer mode) carry
615
+ // reactions as a single `message.reacted` type with an `action` field;
616
+ // higher-level clients split it into `reaction.added`/`reaction.removed`
617
+ // before we see it. All three land here.
618
+ case 'message.reacted':
651
619
  case 'reaction.added':
652
620
  case 'reaction.removed':
653
621
  return {
654
- type: sourceType === 'reaction.added' ? 'reactionAdded' : 'reactionRemoved',
655
- messageId: readString(record, 'messageId', 'message_id') ?? '',
656
- emoji: readString(record, 'emoji') ?? '',
657
- agentName: readString(record, 'agentName', 'agent_name') ?? '',
622
+ type: sourceType === 'reaction.removed' || str(record, 'action') === 'removed'
623
+ ? 'reactionRemoved'
624
+ : 'reactionAdded',
625
+ messageId: str(record, 'message_id') ?? '',
626
+ emoji: str(record, 'emoji') ?? '',
627
+ agentName: str(record, 'agent_name') ?? '',
658
628
  };
659
629
  case 'action.invoked':
660
630
  return {
661
631
  type: 'actionInvoked',
662
- invocationId: readString(record, 'invocationId', 'invocation_id') ?? '',
663
- actionName: readString(record, 'actionName', 'action_name') ?? '',
664
- callerName: readString(record, 'callerName', 'caller_name') ?? '',
665
- handlerAgentId: readString(record, 'handlerAgentId', 'handler_agent_id') ?? '',
632
+ invocationId: str(record, 'invocation_id') ?? '',
633
+ actionName: str(record, 'action_name') ?? '',
634
+ callerName: str(record, 'caller_name') ?? '',
635
+ handlerAgentId: str(record, 'handler_agent_id') ?? '',
666
636
  };
667
637
  case 'open':
668
638
  return { type: 'connected' };
@@ -671,9 +641,9 @@ export function normalizeMessagingEvent(input) {
671
641
  case 'error':
672
642
  return { type: 'error' };
673
643
  case 'reconnecting':
674
- return { type: 'reconnecting', attempt: readNumber(record, 'attempt') ?? 0 };
644
+ return { type: 'reconnecting', attempt: num(record, 'attempt') ?? 0 };
675
645
  case 'permanently_disconnected':
676
- return { type: 'permanentlyDisconnected', attempt: readNumber(record, 'attempt') ?? 0 };
646
+ return { type: 'permanentlyDisconnected', attempt: num(record, 'attempt') ?? 0 };
677
647
  default:
678
648
  return { type: 'unknown', ...(sourceType ? { sourceType } : {}), raw: input };
679
649
  }