@agentchatme/openclaw 0.2.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.
@@ -0,0 +1,756 @@
1
+ import { A as AgentchatChannelConfig } from './setup-entry-CU0vHfyd.js';
2
+ export { a as AGENTCHAT_CHANNEL_ID, b as AGENTCHAT_DEFAULT_ACCOUNT_ID, c as AgentchatResolvedAccount, d as agentchatChannelEntry, e as agentchatPlugin, f as agentchatSetupEntry, e as agentchatSetupPlugin, d as default, p as parseChannelConfig } from './setup-entry-CU0vHfyd.js';
3
+ import { ChannelSetupWizard } from 'openclaw/plugin-sdk/channel-setup';
4
+ export { hasAgentChatConfiguredState } from './configured-state.js';
5
+ import { WebSocket } from 'ws';
6
+ import 'openclaw/plugin-sdk/channel-core';
7
+ import 'zod';
8
+
9
+ /**
10
+ * Minimal HTTP client used by the setup plugin (P7) to:
11
+ * - verify an API key is live before finalizing setup (`validateApiKey`)
12
+ * - drive the email-OTP self-registration flow for agents without a key yet
13
+ * (`registerAgentStart` → `registerAgentVerify`)
14
+ *
15
+ * Why this lives separately from `outbound.ts`:
16
+ * - `outbound.ts` is the hot-path message sender. It wants retries, a circuit
17
+ * breaker, metrics, and a long-lived `fetch`. Setup is one-shot, low-rate,
18
+ * user-interactive — a 200ms validation call doesn't need any of that.
19
+ * - Setup runs before the runtime exists; coupling it to `OutboundAdapter`
20
+ * (which expects a parsed `AgentchatChannelConfig`) would force an odd
21
+ * partial-config path during registration.
22
+ *
23
+ * The server endpoints this module targets are stable AgentChat REST calls:
24
+ * - `GET /v1/agents/me` → 200 OK when the key authenticates
25
+ * - `POST /v1/register` → 200 with `{ pending_id }`
26
+ * - `POST /v1/register/verify` → 201 with `{ agent, api_key }` on success
27
+ *
28
+ * All methods return strongly-typed result unions — setup UIs can `switch` on
29
+ * the discriminant without guessing at HTTP status codes.
30
+ */
31
+ /** Subset of the AgentChat agent row the setup surface needs to show the user. */
32
+ interface AgentchatAgentIdentity {
33
+ readonly handle: string;
34
+ readonly displayName: string | null;
35
+ readonly email: string;
36
+ readonly createdAt: string;
37
+ }
38
+ type ValidateApiKeyResult = {
39
+ readonly ok: true;
40
+ readonly agent: AgentchatAgentIdentity;
41
+ } | {
42
+ readonly ok: false;
43
+ /** High-level reason code the UI can map to a localized message. */
44
+ readonly reason: 'unauthorized' | 'forbidden' | 'deleted' | 'network-error' | 'unreachable' | 'server-error' | 'unexpected-shape';
45
+ readonly message: string;
46
+ readonly status?: number;
47
+ };
48
+ interface ValidateApiKeyOptions {
49
+ readonly apiBase?: string;
50
+ readonly fetch?: typeof fetch;
51
+ readonly timeoutMs?: number;
52
+ }
53
+ /**
54
+ * Probe the API key against `GET /v1/agents/me`. Returns a discriminated
55
+ * result — never throws on HTTP or network errors. The caller decides how
56
+ * to surface each failure mode to the user.
57
+ */
58
+ declare function validateApiKey(apiKey: string, opts?: ValidateApiKeyOptions): Promise<ValidateApiKeyResult>;
59
+ interface RegisterAgentStartInput {
60
+ readonly email: string;
61
+ readonly handle: string;
62
+ readonly displayName?: string;
63
+ readonly description?: string;
64
+ }
65
+ type RegisterStartResult = {
66
+ readonly ok: true;
67
+ readonly pendingId: string;
68
+ } | {
69
+ readonly ok: false;
70
+ readonly reason: 'invalid-handle' | 'handle-taken' | 'email-taken' | 'email-is-owner' | 'email-exhausted' | 'rate-limited' | 'otp-failed' | 'network-error' | 'server-error' | 'validation';
71
+ readonly message: string;
72
+ readonly status?: number;
73
+ readonly retryAfterSeconds?: number;
74
+ };
75
+ interface RegisterAgentVerifyInput {
76
+ readonly pendingId: string;
77
+ readonly code: string;
78
+ }
79
+ type RegisterVerifyResult = {
80
+ readonly ok: true;
81
+ readonly apiKey: string;
82
+ readonly agent: AgentchatAgentIdentity;
83
+ } | {
84
+ readonly ok: false;
85
+ readonly reason: 'expired' | 'invalid-code' | 'rate-limited' | 'handle-taken' | 'email-taken' | 'email-is-owner' | 'network-error' | 'server-error' | 'unexpected-shape' | 'validation';
86
+ readonly message: string;
87
+ readonly status?: number;
88
+ readonly retryAfterSeconds?: number;
89
+ };
90
+ interface RegisterOptions {
91
+ readonly apiBase?: string;
92
+ readonly fetch?: typeof fetch;
93
+ readonly timeoutMs?: number;
94
+ }
95
+ /**
96
+ * Kick off an email-OTP registration. Server caches the intent under a
97
+ * `pending_id` for 10 minutes and emails a 6-digit code to `email`.
98
+ */
99
+ declare function registerAgentStart(input: RegisterAgentStartInput, opts?: RegisterOptions): Promise<RegisterStartResult>;
100
+ /** Verify the OTP the user received by email and mint the API key. */
101
+ declare function registerAgentVerify(input: RegisterAgentVerifyInput, opts?: RegisterOptions): Promise<RegisterVerifyResult>;
102
+ /**
103
+ * Throw-flavored wrapper used by callers that prefer exception control flow
104
+ * (the setup plugin's `afterAccountConfigWritten` hook). Converts a failure
105
+ * result into an `AgentChatChannelError` with an appropriate class.
106
+ */
107
+ declare function assertApiKeyValid(apiKey: string, opts?: ValidateApiKeyOptions): Promise<AgentchatAgentIdentity>;
108
+
109
+ /**
110
+ * Interactive setup wizard for the AgentChat channel.
111
+ *
112
+ * Invoked by `openclaw channels add --channel agentchat`. Drives three branches:
113
+ *
114
+ * - edit — existing config → re-validate OR rotate OR change API base
115
+ * - have-key — user pastes an existing ac_live_* key → /agents/me probe → write
116
+ * - register — email + handle + description → /register + OTP → /register/verify
117
+ *
118
+ * All branches converge on `writeAccountPatch` which mutates
119
+ * `channels.agentchat.<accountId>` in either flat or multi-account form.
120
+ *
121
+ * Every server-side failure mode surfaces as actionable copy — there are no
122
+ * silent failures. The wizard only returns when the user has either finished
123
+ * or explicitly chosen to abort.
124
+ */
125
+
126
+ declare const agentchatSetupWizard: ChannelSetupWizard;
127
+
128
+ interface Logger {
129
+ trace(obj: object, msg?: string): void;
130
+ debug(obj: object, msg?: string): void;
131
+ info(obj: object, msg?: string): void;
132
+ warn(obj: object, msg?: string): void;
133
+ error(obj: object, msg?: string): void;
134
+ child(bindings: Record<string, unknown>): Logger;
135
+ }
136
+
137
+ /**
138
+ * Optional Prometheus metrics for the channel plugin.
139
+ *
140
+ * Design: the gateway owns the `/metrics` endpoint. Plugins do not expose
141
+ * their own port. If the user passes a `prom-client` Registry via channel
142
+ * runtime context, we register our counters/gauges/histograms into it.
143
+ * Otherwise, we return a no-op recorder — zero overhead, zero config.
144
+ *
145
+ * Names are prefixed with `agentchat_channel_` for easy scraping.
146
+ */
147
+ interface MetricsRecorder {
148
+ /** Incremented on every successfully delivered inbound message. */
149
+ incInboundDelivered(labels: {
150
+ kind: InboundKind;
151
+ }): void;
152
+ /** Incremented on every successfully sent outbound message. */
153
+ incOutboundSent(labels: {
154
+ kind: OutboundKind;
155
+ }): void;
156
+ /** Incremented on every send failure, labeled by error class. */
157
+ incOutboundFailed(labels: {
158
+ errorClass: string;
159
+ }): void;
160
+ /** Incremented on every reconnect attempt. */
161
+ incReconnect(labels: {
162
+ reason: string;
163
+ }): void;
164
+ /** Observe end-to-end send latency (ms). */
165
+ observeSendLatency(ms: number): void;
166
+ /** Set the current connection state (cardinality bounded by state kinds). */
167
+ setConnectionState(kind: string): void;
168
+ /** Set the current in-flight send queue depth. */
169
+ setInFlightDepth(n: number): void;
170
+ }
171
+ type InboundKind = 'message' | 'group-message' | 'typing' | 'read' | 'presence' | 'rate-limit-warning';
172
+ type OutboundKind = 'message' | 'typing' | 'read';
173
+
174
+ /**
175
+ * Error taxonomy for the AgentChat channel plugin.
176
+ *
177
+ * Every error that crosses a pipeline boundary (inbound receive, outbound
178
+ * send, config validation, transport) must be classifiable as one of:
179
+ *
180
+ * terminal-auth — 401/403. API key invalid or revoked. Do not retry.
181
+ * Move state machine to AUTH_FAIL.
182
+ * terminal-user — 400/422 (client-side validation). Bug in this plugin
183
+ * or malformed outbound. Log at error level, drop,
184
+ * do not retry.
185
+ * retry-rate — 429. Retry after `retryAfterMs` (or jittered
186
+ * exponential backoff if header absent).
187
+ * retry-transient — 5xx, ECONNRESET, ETIMEDOUT, network flap.
188
+ * Exponential backoff + jitter. Open circuit after N.
189
+ * idempotent-replay — 409 on POST /v1/messages with duplicate
190
+ * client_msg_id. Treat as success, log at info.
191
+ * validation — Zod schema failed on inbound payload. Likely a
192
+ * server change. Log at error, drop, alert.
193
+ */
194
+ type ErrorClass = 'terminal-auth' | 'terminal-user' | 'retry-rate' | 'retry-transient' | 'idempotent-replay' | 'validation';
195
+ declare class AgentChatChannelError extends Error {
196
+ readonly class_: ErrorClass;
197
+ readonly retryAfterMs: number | undefined;
198
+ readonly statusCode: number | undefined;
199
+ constructor(class_: ErrorClass, message: string, options?: {
200
+ cause?: unknown;
201
+ retryAfterMs?: number;
202
+ statusCode?: number;
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Internal shared types for @agentchatme/openclaw.
208
+ *
209
+ * Kept intentionally small. Anything externally visible is exported from
210
+ * `./index.ts` or `./setup-entry.ts`. Types with a public SDK shape (channel
211
+ * plugin, setup plugin) live next to their respective entry files.
212
+ */
213
+ type UnixMillis = number;
214
+
215
+ /**
216
+ * Connection state machine for the AgentChat WebSocket.
217
+ *
218
+ * Pure functions only — no I/O, no timers. The runtime (P2) owns the socket
219
+ * and drives transitions by emitting events.
220
+ *
221
+ * States:
222
+ * DISCONNECTED — initial, and after a clean close
223
+ * CONNECTING — TCP/TLS handshake + WS upgrade in flight
224
+ * AUTHENTICATING — socket open, waiting for `hello.ok` from server
225
+ * READY — authenticated, receiving + sending normally
226
+ * DEGRADED — heartbeat missed or send buffer full; still connected but impaired
227
+ * DRAINING — SIGTERM received; flush in-flight sends, then close
228
+ * CLOSED — terminal after clean shutdown
229
+ * AUTH_FAIL — terminal after 401/403; operator action required (rotate key)
230
+ *
231
+ * Invariants:
232
+ * - From AUTH_FAIL, no event can move us back to CONNECTING without
233
+ * an explicit RECONFIGURED event (operator rotated key).
234
+ * - From CLOSED, only SHUTDOWN is a no-op; anything else is a bug.
235
+ * - DRAINING has a deadline (ms timestamp) after which we force-close.
236
+ */
237
+
238
+ type ConnectionState = {
239
+ readonly kind: 'DISCONNECTED';
240
+ } | {
241
+ readonly kind: 'CONNECTING';
242
+ readonly attempt: number;
243
+ readonly startedAt: UnixMillis;
244
+ } | {
245
+ readonly kind: 'AUTHENTICATING';
246
+ readonly startedAt: UnixMillis;
247
+ } | {
248
+ readonly kind: 'READY';
249
+ readonly connectedAt: UnixMillis;
250
+ } | {
251
+ readonly kind: 'DEGRADED';
252
+ readonly since: UnixMillis;
253
+ readonly reason: string;
254
+ } | {
255
+ readonly kind: 'DRAINING';
256
+ readonly since: UnixMillis;
257
+ readonly deadline: UnixMillis;
258
+ } | {
259
+ readonly kind: 'CLOSED';
260
+ } | {
261
+ readonly kind: 'AUTH_FAIL';
262
+ readonly reason: string;
263
+ };
264
+
265
+ /**
266
+ * AgentChat WebSocket client — state-machine-driven, production-grade.
267
+ *
268
+ * Responsibilities:
269
+ * - Own the WS transport and HELLO handshake lifecycle.
270
+ * - Drive the connection state machine (`./state-machine.ts`) by emitting
271
+ * events in response to socket/auth/heartbeat transitions.
272
+ * - Maintain native WS PING/PONG heartbeat at `ping.intervalMs`; declare
273
+ * DEGRADED on `ping.timeoutMs` miss, then force-reconnect.
274
+ * - Schedule reconnect with exponential backoff + ±`jitterRatio` jitter
275
+ * per `reconnect.*` config. Thundering-herd-safe across a fleet.
276
+ * - Emit typed events upwards (`inboundFrame`, `stateChanged`, `error`)
277
+ * so the normalizer (P3) and outbound adapter (P4) can consume them.
278
+ *
279
+ * Intentional design choices:
280
+ * - Uses the `ws` package directly (not `globalThis.WebSocket`) because
281
+ * OpenClaw runs on Node — `ws` exposes `.ping()` / `.pong()` / event
282
+ * hooks that native WebSocket omits. Every major agent-hosting target
283
+ * (Fly, Render, Vercel functions, Lambda) is Node.
284
+ * - Authenticates via HELLO frame (`{type: "hello", api_key}`), never via
285
+ * URL query string. Keeps the key out of access logs and Referers.
286
+ * - `RECONNECT_HARD_CAP_ATTEMPTS` guards against runaway reconnect loops
287
+ * after catastrophic network events; crosses into `AUTH_FAIL` so the
288
+ * operator sees a clear signal instead of silent retry forever.
289
+ * - Frame parsing is defensive: malformed JSON, unknown `type`, missing
290
+ * payload — all classified as `validation` errors (non-terminal for
291
+ * the connection, but dropped with a metric + log).
292
+ * - No message-layer ordering here. Per-conversation seq ordering + gap
293
+ * recovery belong to the SDK's `RealtimeClient` and the drain layer
294
+ * (P3 consumers). This module is strictly the transport.
295
+ */
296
+
297
+ /** One parsed inbound server frame. */
298
+ interface InboundFrame {
299
+ readonly type: string;
300
+ readonly payload: Record<string, unknown>;
301
+ readonly receivedAt: UnixMillis;
302
+ readonly raw: unknown;
303
+ }
304
+ /** One client-action frame the caller wants to push. */
305
+ interface OutboundFrame {
306
+ readonly type: string;
307
+ readonly payload: Record<string, unknown>;
308
+ readonly id?: string;
309
+ }
310
+ interface WsClientEvents {
311
+ stateChanged: (next: ConnectionState, prev: ConnectionState) => void;
312
+ inboundFrame: (frame: InboundFrame) => void;
313
+ error: (error: AgentChatChannelError) => void;
314
+ /** Fired once per `hello.ok` — useful for triggering a /v1/messages/sync drain upstream. */
315
+ authenticated: (at: UnixMillis) => void;
316
+ /** Fired after a clean close + CLOSED state (after stop() or terminal auth failure). */
317
+ closed: () => void;
318
+ }
319
+ interface WsClientOptions {
320
+ readonly config: AgentchatChannelConfig;
321
+ readonly logger: Logger;
322
+ readonly metrics: MetricsRecorder;
323
+ /** Clock source — override in tests. Default: `Date.now`. */
324
+ readonly now?: () => UnixMillis;
325
+ /** WebSocket constructor override — tests inject a mock. Default: `ws`. */
326
+ readonly webSocketCtor?: typeof WebSocket;
327
+ /** Random source for jitter — override in tests. Default: `Math.random`. */
328
+ readonly random?: () => number;
329
+ /** timer.setTimeout override for tests. */
330
+ readonly setTimeout?: (fn: () => void, ms: number) => NodeJS.Timeout;
331
+ readonly clearTimeout?: (h: NodeJS.Timeout) => void;
332
+ readonly setInterval?: (fn: () => void, ms: number) => NodeJS.Timeout;
333
+ readonly clearInterval?: (h: NodeJS.Timeout) => void;
334
+ }
335
+ type Listener<E extends keyof WsClientEvents> = WsClientEvents[E];
336
+ /**
337
+ * AgentChat channel WebSocket client.
338
+ *
339
+ * Lifecycle:
340
+ * 1. `start()` — DISCONNECTED → CONNECTING. Transport opens, HELLO is sent.
341
+ * 2. `hello.ok` → READY. Heartbeat timer starts. Upstream consumers begin.
342
+ * 3. On socket drop → DISCONNECTED → CONNECTING (with backoff).
343
+ * 4. `stop(deadlineMs)` → DRAINING. Wait for in-flight drains, then CLOSED.
344
+ *
345
+ * Thread-safety: this class is single-threaded. All callbacks fire on the
346
+ * Node event loop; callers should never invoke public methods from timer
347
+ * callbacks without being aware of reentrancy (e.g., a `stateChanged`
348
+ * listener that calls `send()` is fine, but `start()` inside `closed()`
349
+ * will re-enter the lifecycle — usually a bug).
350
+ */
351
+ declare class AgentchatWsClient {
352
+ private readonly config;
353
+ private readonly logger;
354
+ private readonly metrics;
355
+ private readonly now;
356
+ private readonly WebSocketCtor;
357
+ private readonly random;
358
+ private readonly _setTimeout;
359
+ private readonly _clearTimeout;
360
+ private readonly _setInterval;
361
+ private readonly _clearInterval;
362
+ private state;
363
+ private ws;
364
+ private attempt;
365
+ private helloAckTimer;
366
+ private pingTimer;
367
+ private pongTimer;
368
+ private reconnectTimer;
369
+ private drainTimer;
370
+ private readonly listeners;
371
+ constructor(options: WsClientOptions);
372
+ /** Current connection state. Read-only from the caller's perspective. */
373
+ getState(): ConnectionState;
374
+ /**
375
+ * Kick off connection. No-op if already connecting, authenticating, or
376
+ * ready. Throws on `AUTH_FAIL` — the caller must call `reconfigured()`
377
+ * after rotating the API key before retrying.
378
+ */
379
+ start(): void;
380
+ /**
381
+ * Push a client-action frame. Requires `canSend(state)` — returns false
382
+ * if the socket is not ready. Callers should queue and retry from the
383
+ * outbound adapter (P4) rather than drop here.
384
+ */
385
+ send(frame: OutboundFrame): boolean;
386
+ /**
387
+ * Request a graceful drain. The caller-supplied deadline is a Unix-ms
388
+ * timestamp; after it passes, the socket is force-closed regardless of
389
+ * remaining in-flight work. Safe to call from any state.
390
+ */
391
+ stop(deadline?: UnixMillis): void;
392
+ /**
393
+ * Signal that operator has rotated the API key. Transitions from
394
+ * AUTH_FAIL back to DISCONNECTED; the next `start()` call will attempt
395
+ * a fresh HELLO with the current config.
396
+ */
397
+ reconfigured(): void;
398
+ on<E extends keyof WsClientEvents>(event: E, handler: Listener<E>): () => void;
399
+ private openSocket;
400
+ private bindSocket;
401
+ private handleOpen;
402
+ private handleMessage;
403
+ private extractReason;
404
+ private extractPayload;
405
+ private handleHelloOk;
406
+ private handleAuthRejected;
407
+ private handleClose;
408
+ private closeSocket;
409
+ private startHeartbeat;
410
+ private stopHeartbeat;
411
+ private sendPing;
412
+ private handlePong;
413
+ private scheduleReconnect;
414
+ private computeReconnectDelay;
415
+ private scheduleDrainDeadline;
416
+ /**
417
+ * Called by upper layers when their pending work has fully drained, to
418
+ * let the WS client transition CLOSED earlier than the deadline.
419
+ */
420
+ drainCompleted(): void;
421
+ private applyEvent;
422
+ private emit;
423
+ private emitError;
424
+ }
425
+
426
+ /**
427
+ * Inbound normalizer — AgentChat server events → channel-neutral shape.
428
+ *
429
+ * Responsibilities:
430
+ * - Validate every server frame against a Zod schema. Unknown shapes
431
+ * surface as `validation` errors (the transport catches them and drops
432
+ * the frame without killing the connection).
433
+ * - Translate AgentChat-native payloads into a stable, plugin-internal
434
+ * `NormalizedInbound` shape. The OpenClaw channel binding (P7 runtime
435
+ * wiring) adapts this further to the gateway's `ChannelInboundMessage`
436
+ * contract.
437
+ * - Classify conversation kind (direct vs group) from the `dir_*`/`grp_*`
438
+ * prefix convention so consumers don't need to grep prefixes themselves.
439
+ *
440
+ * Why this module exists (rather than passing raw frames up):
441
+ * - The WS client is pure transport; it shouldn't know what
442
+ * `message.new` means semantically.
443
+ * - OpenClaw's ChannelInboundMessage has a specific shape (thread id,
444
+ * content parts, attachments, sender identity). Converting from
445
+ * AgentChat's wire format in one place is the only sane way to keep
446
+ * the mapping auditable.
447
+ * - Per-event Zod schemas make drift between plugin and server obvious
448
+ * and loud — a server field rename surfaces as a validation error
449
+ * caught in CI long before it goes to prod.
450
+ */
451
+
452
+ type ConversationKind = 'direct' | 'group';
453
+ interface NormalizedMessage {
454
+ readonly kind: 'message';
455
+ readonly conversationKind: ConversationKind;
456
+ readonly conversationId: string;
457
+ /** Sender handle. */
458
+ readonly sender: string;
459
+ readonly messageId: string;
460
+ readonly clientMsgId: string;
461
+ readonly seq: number;
462
+ readonly messageType: 'text' | 'structured' | 'file' | 'system';
463
+ readonly content: {
464
+ readonly text?: string;
465
+ readonly data?: Record<string, unknown>;
466
+ readonly attachmentId?: string;
467
+ };
468
+ readonly metadata: Record<string, unknown>;
469
+ /**
470
+ * Per-recipient delivery state. Since migration 011, per-recipient state
471
+ * lives on `message_deliveries`, not the message row — so fresh-send
472
+ * `message.new` envelopes have no status field. Drain-path (sync /
473
+ * undelivered) envelopes still include `'stored'`. `null` means the
474
+ * server didn't attach one; callers that need the authoritative state
475
+ * should query `GET /v1/messages/sync` or `/delivery-receipts`.
476
+ */
477
+ readonly status: 'stored' | 'delivered' | 'read' | null;
478
+ readonly createdAt: string;
479
+ readonly deliveredAt: string | null;
480
+ readonly readAt: string | null;
481
+ readonly receivedAt: UnixMillis;
482
+ }
483
+ interface NormalizedReadReceipt {
484
+ readonly kind: 'read-receipt';
485
+ readonly conversationKind: ConversationKind;
486
+ readonly conversationId: string;
487
+ readonly reader: string;
488
+ readonly throughSeq: number;
489
+ readonly at: string | null;
490
+ readonly receivedAt: UnixMillis;
491
+ }
492
+ interface NormalizedTyping {
493
+ readonly kind: 'typing';
494
+ readonly action: 'start' | 'stop';
495
+ readonly conversationKind: ConversationKind;
496
+ readonly conversationId: string;
497
+ readonly sender: string;
498
+ readonly receivedAt: UnixMillis;
499
+ }
500
+ interface NormalizedPresence {
501
+ readonly kind: 'presence';
502
+ readonly handle: string;
503
+ readonly status: 'online' | 'away' | 'offline';
504
+ readonly lastActiveAt: string | null;
505
+ readonly customStatus: string | null;
506
+ readonly receivedAt: UnixMillis;
507
+ }
508
+ interface NormalizedRateLimitWarning {
509
+ readonly kind: 'rate-limit-warning';
510
+ readonly endpoint: string | null;
511
+ readonly limit: number | null;
512
+ readonly remaining: number | null;
513
+ readonly resetAt: string | null;
514
+ readonly message: string | null;
515
+ readonly receivedAt: UnixMillis;
516
+ }
517
+ interface NormalizedGroupInvite {
518
+ readonly kind: 'group-invite';
519
+ readonly inviteId: string;
520
+ readonly groupId: string;
521
+ readonly groupName: string;
522
+ readonly groupDescription: string | null;
523
+ readonly groupAvatarUrl: string | null;
524
+ readonly groupMemberCount: number;
525
+ readonly inviterHandle: string;
526
+ readonly createdAt: string;
527
+ readonly receivedAt: UnixMillis;
528
+ }
529
+ interface NormalizedGroupDeleted {
530
+ readonly kind: 'group-deleted';
531
+ readonly groupId: string;
532
+ readonly deletedByHandle: string;
533
+ readonly deletedAt: string;
534
+ readonly receivedAt: UnixMillis;
535
+ }
536
+ /**
537
+ * Unknown server event — arrived over the wire but isn't in our explicit
538
+ * handler set. We still surface it (the gateway may want to log it or
539
+ * forward it), but with a `type` field so downstream can discriminate.
540
+ */
541
+ interface NormalizedUnknown {
542
+ readonly kind: 'unknown';
543
+ readonly type: string;
544
+ readonly payload: Record<string, unknown>;
545
+ readonly receivedAt: UnixMillis;
546
+ }
547
+ type NormalizedInbound = NormalizedMessage | NormalizedReadReceipt | NormalizedTyping | NormalizedPresence | NormalizedRateLimitWarning | NormalizedGroupInvite | NormalizedGroupDeleted | NormalizedUnknown;
548
+
549
+ /**
550
+ * Shape of a message record as returned by the AgentChat REST API.
551
+ *
552
+ * Mirrors the `Message` interface defined in the AgentChat TypeScript SDK
553
+ * (`packages/sdk-typescript/src/types/message.ts`). We keep a local copy so
554
+ * this plugin has no runtime dependency on the SDK — it's type-only at the
555
+ * source and the SDK isn't published to npm yet. If the server's message
556
+ * contract changes, update both files together; a drift is a schema
557
+ * mismatch the server will catch, but we'd rather fail type-check first.
558
+ */
559
+ type MessageType = 'text' | 'structured' | 'file' | 'system';
560
+ type MessageStatus = 'stored' | 'delivered' | 'read';
561
+ interface MessageContent {
562
+ text?: string;
563
+ data?: Record<string, unknown>;
564
+ attachment_id?: string;
565
+ }
566
+ interface Message {
567
+ id: string;
568
+ conversation_id: string;
569
+ sender: string;
570
+ client_msg_id: string;
571
+ seq: number;
572
+ type: MessageType;
573
+ content: MessageContent;
574
+ metadata: Record<string, unknown>;
575
+ status: MessageStatus;
576
+ created_at: string;
577
+ delivered_at: string | null;
578
+ read_at: string | null;
579
+ }
580
+
581
+ /**
582
+ * Outbound adapter — OpenClaw send → AgentChat `POST /v1/messages`.
583
+ *
584
+ * Pipeline (left to right, each step is independently observable):
585
+ * 1. Caller invokes `sendMessage(input)` with a channel-neutral shape.
586
+ * 2. We derive a stable `client_msg_id` (idempotency key). If the caller
587
+ * supplied one, we respect it; otherwise we mint a UUID.
588
+ * 3. Circuit-breaker precheck. Open circuit → fast-fail `retry-transient`.
589
+ * 4. `retryWithPolicy(...)` wraps the actual HTTP call.
590
+ * 5. On success, metrics/logs + return the `Message` the server minted.
591
+ * 6. On 409 (idempotent-replay), treat as success with the replay body.
592
+ * 7. On 429, honor `Retry-After` (seconds or HTTP-date) via the retry
593
+ * policy. Surface as `retry-rate`.
594
+ * 8. On 5xx / network flap, retry with jittered exponential backoff.
595
+ * 9. On 4xx other than 429/409, classify as `terminal-user` → no retry.
596
+ * 10. On 401/403 → `terminal-auth`. We signal upstream so the WS client
597
+ * moves to AUTH_FAIL and the operator gets paged.
598
+ *
599
+ * Backpressure:
600
+ * - An in-flight semaphore bounds concurrent sends to `outbound.maxInFlight`.
601
+ * - Over-threshold sends are enqueued and drained as capacity frees up.
602
+ * - Queue has its own hard cap (`10 * maxInFlight`); over-cap sends reject
603
+ * immediately with `retry-transient` so the caller can shed load rather
604
+ * than OOM.
605
+ */
606
+
607
+ type OutboundMessageInput = OutboundDirectMessage | OutboundGroupMessage;
608
+ interface OutboundMessageBase {
609
+ /**
610
+ * Client-generated idempotency key. Optional — we mint one if absent.
611
+ * Retries of the same logical send MUST reuse the same value or you'll
612
+ * get duplicates.
613
+ */
614
+ readonly clientMsgId?: string;
615
+ readonly type?: 'text' | 'structured' | 'file';
616
+ readonly content: {
617
+ readonly text?: string;
618
+ readonly data?: Record<string, unknown>;
619
+ readonly attachmentId?: string;
620
+ };
621
+ readonly metadata?: Record<string, unknown>;
622
+ /** Correlation id carried through logs and metrics. */
623
+ readonly correlationId?: string;
624
+ }
625
+ interface OutboundDirectMessage extends OutboundMessageBase {
626
+ readonly kind: 'direct';
627
+ /** Recipient handle (e.g. "alice"). */
628
+ readonly to: string;
629
+ }
630
+ interface OutboundGroupMessage extends OutboundMessageBase {
631
+ readonly kind: 'group';
632
+ /** `grp_*` conversation id. */
633
+ readonly conversationId: string;
634
+ }
635
+ interface OutboundBacklogWarning {
636
+ readonly recipientHandle: string;
637
+ readonly undeliveredCount: number;
638
+ }
639
+ interface SendResult {
640
+ readonly message: Message;
641
+ readonly backlogWarning: OutboundBacklogWarning | null;
642
+ readonly idempotentReplay: boolean;
643
+ readonly attempts: number;
644
+ readonly latencyMs: number;
645
+ readonly requestId: string | null;
646
+ }
647
+
648
+ /**
649
+ * AgentChat channel runtime — single lifecycle object that ties the WS
650
+ * transport (P2), inbound normalizer (P3), and outbound adapter (P4)
651
+ * together. This is the surface an OpenClaw channel binding consumes.
652
+ *
653
+ * Responsibilities:
654
+ * - Instantiate the WS client, hook its `inboundFrame` event through
655
+ * `normalizeInbound` and into user-supplied handlers.
656
+ * - Expose `sendMessage` that delegates to the OutboundAdapter.
657
+ * - Coordinate drain: `stop(deadline)` waits for outbound in-flight to
658
+ * clear (up to the deadline) before closing the WS.
659
+ * - Surface a single `getHealth()` snapshot combining connection state +
660
+ * outbound queue depth + circuit breaker state.
661
+ *
662
+ * Not in scope here:
663
+ * - Per-conversation seq ordering and gap recovery — the SDK's
664
+ * `RealtimeClient` owns that. For the channel plugin, we deliver
665
+ * inbound frames as they arrive; upper layers can call
666
+ * `GET /v1/messages/sync` for reconciliation if they require strict
667
+ * ordering across reconnects.
668
+ * - OpenClaw ChannelInboundMessage translation — that lives in the
669
+ * bridge module (consumed by the gateway), not here. Keeping this
670
+ * module decoupled means it can be smoke-tested without OpenClaw.
671
+ */
672
+
673
+ interface ChannelRuntimeHandlers {
674
+ /** Called for every successfully normalized inbound event. */
675
+ onInbound?: (event: NormalizedInbound) => void | Promise<void>;
676
+ /** Called whenever a frame fails validation. Default: log-and-drop. */
677
+ onValidationError?: (error: AgentChatChannelError, frame: InboundFrame) => void;
678
+ /** Called on state transitions — surface to gateway for health/UI. */
679
+ onStateChanged?: (next: ConnectionState, prev: ConnectionState) => void;
680
+ /** Called once per successful HELLO_OK (initial + every reconnect). */
681
+ onAuthenticated?: (at: UnixMillis) => void;
682
+ /** Called on every closed/unreachable situation worth observing. */
683
+ onError?: (error: AgentChatChannelError) => void;
684
+ /** Called when the server signals a backlog warning on an outbound send. */
685
+ onBacklogWarning?: (warning: OutboundBacklogWarning) => void;
686
+ }
687
+ interface ChannelRuntimeOptions {
688
+ readonly config: AgentchatChannelConfig;
689
+ readonly handlers?: ChannelRuntimeHandlers;
690
+ /** Pre-built logger — if absent, we construct one from `config.observability`. */
691
+ readonly logger?: Logger;
692
+ /** Pre-built metrics recorder — if absent, uses no-op. */
693
+ readonly metrics?: MetricsRecorder;
694
+ readonly fetch?: typeof fetch;
695
+ /** Injected for tests. */
696
+ readonly webSocketCtor?: ConstructorParameters<typeof AgentchatWsClient>[0]['webSocketCtor'];
697
+ readonly now?: () => UnixMillis;
698
+ readonly random?: () => number;
699
+ readonly sleep?: (ms: number) => Promise<void>;
700
+ }
701
+ interface HealthSnapshot {
702
+ readonly state: ConnectionState;
703
+ readonly authenticated: boolean;
704
+ readonly outbound: {
705
+ readonly inFlight: number;
706
+ readonly queued: number;
707
+ readonly circuitState: 'closed' | 'open' | 'half-open';
708
+ };
709
+ }
710
+ /**
711
+ * Orchestrates the full channel. Construct once per (channelId, accountId)
712
+ * pair at gateway startup; call `start()` to bring it up, `stop()` to
713
+ * drain. Dispose is folded into `stop()` — after it resolves, the
714
+ * instance is terminal.
715
+ */
716
+ declare class AgentchatChannelRuntime {
717
+ private readonly config;
718
+ private readonly handlers;
719
+ private readonly logger;
720
+ private readonly metrics;
721
+ private readonly ws;
722
+ private readonly outbound;
723
+ private readonly now;
724
+ private started;
725
+ private authenticated;
726
+ private stopPromise;
727
+ constructor(opts: ChannelRuntimeOptions);
728
+ /** Open the transport. Idempotent — subsequent calls are no-ops. */
729
+ start(): void;
730
+ /**
731
+ * Graceful shutdown. Waits for outbound in-flight to drain up to the
732
+ * deadline, then force-closes the WS. Returns a promise that resolves
733
+ * once the WS has emitted `closed`.
734
+ */
735
+ stop(deadlineMs?: UnixMillis): Promise<void>;
736
+ private pollUntilIdle;
737
+ /**
738
+ * Send an outbound message. Delegates to the OutboundAdapter; callers
739
+ * should handle `AgentChatChannelError` with class dispatch.
740
+ */
741
+ sendMessage(input: OutboundMessageInput): Promise<SendResult>;
742
+ /** Push a client-action frame over the WS (typing, read-ack, presence). */
743
+ sendWsAction(type: 'typing.start' | 'typing.stop' | 'message.read_ack' | 'presence.update', payload: Record<string, unknown>): boolean;
744
+ /**
745
+ * Operator has rotated the API key — signal the WS client so it can
746
+ * exit AUTH_FAIL. The caller is responsible for creating a new runtime
747
+ * with the updated config OR for calling this after config hot-reload.
748
+ */
749
+ reconfigured(): void;
750
+ getHealth(): HealthSnapshot;
751
+ private bindWsEvents;
752
+ private dispatchFrame;
753
+ private recordInboundMetric;
754
+ }
755
+
756
+ export { AgentChatChannelError, type AgentchatAgentIdentity, AgentchatChannelConfig, AgentchatChannelRuntime, type ChannelRuntimeHandlers, type ChannelRuntimeOptions, type ConnectionState, type ConversationKind, type ErrorClass, type HealthSnapshot, type Message, type MessageContent, type MessageStatus, type MessageType, type NormalizedGroupDeleted, type NormalizedGroupInvite, type NormalizedInbound, type NormalizedMessage, type NormalizedPresence, type NormalizedRateLimitWarning, type NormalizedReadReceipt, type NormalizedTyping, type NormalizedUnknown, type OutboundBacklogWarning, type OutboundDirectMessage, type OutboundGroupMessage, type OutboundMessageInput, type RegisterAgentStartInput, type RegisterAgentVerifyInput, type RegisterOptions, type RegisterStartResult, type RegisterVerifyResult, type SendResult, type ValidateApiKeyOptions, type ValidateApiKeyResult, agentchatSetupWizard, assertApiKeyValid, registerAgentStart, registerAgentVerify, validateApiKey };