@pingagent/sdk 0.1.6 → 0.1.7

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.
package/dist/index.d.ts CHANGED
@@ -486,8 +486,19 @@ interface WsSubscriptionOptions {
486
486
  onMessage: (envelope: any, conversationId: string) => void;
487
487
  onControl?: (control: WsControlPayload, conversationId: string) => void;
488
488
  onError?: (err: Error) => void;
489
+ /** Optional debug hook for observability (non-fatal events). */
490
+ onDebug?: (info: {
491
+ event: string;
492
+ conversationId?: string;
493
+ detail?: Record<string, unknown>;
494
+ }) => void;
489
495
  /** Called when a WebSocket opens or reopens; e.g. Skill can run catchUp to fill gaps. */
490
496
  onOpen?: (conversationId: string) => void;
497
+ /**
498
+ * When true (default), drop ws_message envelopes sent by myself (sender_did === myDid).
499
+ * Set to false when you intentionally run multiple devices/processes under the same DID and want cross-device delivery.
500
+ */
501
+ ignoreSelfMessages?: boolean;
491
502
  /**
492
503
  * Keepalive heartbeat for idle connections to reduce "WS silently dropped" periods.
493
504
  * This is client-side only (WS ping/pong control frames) and does not add inbox HTTP fetch cost.
@@ -517,6 +528,8 @@ declare class WsSubscription {
517
528
  private stopped;
518
529
  /** Conversation IDs that were explicitly stopped (e.g. revoke); do not reconnect. */
519
530
  private stoppedConversations;
531
+ private lastParseErrorAtByConv;
532
+ private lastFrameAtByConv;
520
533
  private heartbeatTimer;
521
534
  private heartbeatStates;
522
535
  constructor(opts: WsSubscriptionOptions);
@@ -531,6 +544,7 @@ declare class WsSubscription {
531
544
  private connectAsync;
532
545
  private heartbeatTick;
533
546
  private scheduleReconnect;
547
+ private isSubscribableConversationType;
534
548
  private connectAll;
535
549
  private syncConnections;
536
550
  }
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  loadIdentity,
17
17
  saveIdentity,
18
18
  updateStoredToken
19
- } from "./chunk-OHS3SA5V.js";
19
+ } from "./chunk-OVLKR4JF.js";
20
20
  export {
21
21
  A2AAdapter,
22
22
  ContactManager,
@@ -5,7 +5,7 @@ import {
5
5
  ensureTokenValid,
6
6
  loadIdentity,
7
7
  updateStoredToken
8
- } from "./chunk-OHS3SA5V.js";
8
+ } from "./chunk-OVLKR4JF.js";
9
9
 
10
10
  // src/web-server.ts
11
11
  import * as fs from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pingagent/sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -26,9 +26,9 @@
26
26
  "commander": "^13.0.0",
27
27
  "uuid": "^11.0.0",
28
28
  "ws": "^8.0.0",
29
- "@pingagent/protocol": "0.1.1",
30
29
  "@pingagent/schemas": "0.1.1",
31
- "@pingagent/a2a": "0.1.1"
30
+ "@pingagent/a2a": "0.1.1",
31
+ "@pingagent/protocol": "0.1.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/better-sqlite3": "^7.6.0",
@@ -19,8 +19,15 @@ export interface WsSubscriptionOptions {
19
19
  onMessage: (envelope: any, conversationId: string) => void;
20
20
  onControl?: (control: WsControlPayload, conversationId: string) => void;
21
21
  onError?: (err: Error) => void;
22
+ /** Optional debug hook for observability (non-fatal events). */
23
+ onDebug?: (info: { event: string; conversationId?: string; detail?: Record<string, unknown> }) => void;
22
24
  /** Called when a WebSocket opens or reopens; e.g. Skill can run catchUp to fill gaps. */
23
25
  onOpen?: (conversationId: string) => void;
26
+ /**
27
+ * When true (default), drop ws_message envelopes sent by myself (sender_did === myDid).
28
+ * Set to false when you intentionally run multiple devices/processes under the same DID and want cross-device delivery.
29
+ */
30
+ ignoreSelfMessages?: boolean;
24
31
 
25
32
  /**
26
33
  * Keepalive heartbeat for idle connections to reduce "WS silently dropped" periods.
@@ -67,6 +74,8 @@ export class WsSubscription {
67
74
  private stopped = false;
68
75
  /** Conversation IDs that were explicitly stopped (e.g. revoke); do not reconnect. */
69
76
  private stoppedConversations = new Set<string>();
77
+ private lastParseErrorAtByConv = new Map<string, number>();
78
+ private lastFrameAtByConv = new Map<string, number>();
70
79
 
71
80
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
72
81
  private heartbeatStates = new Map<
@@ -119,6 +128,8 @@ export class WsSubscription {
119
128
  }
120
129
  this.reconnectAttempts.clear();
121
130
  this.stoppedConversations.clear();
131
+ this.lastParseErrorAtByConv.clear();
132
+ this.lastFrameAtByConv.clear();
122
133
 
123
134
  for (const state of this.heartbeatStates.values()) {
124
135
  if (state.pongTimeout) clearTimeout(state.pongTimeout);
@@ -150,6 +161,8 @@ export class WsSubscription {
150
161
  }
151
162
  this.connections.delete(conversationId);
152
163
  }
164
+ this.lastParseErrorAtByConv.delete(conversationId);
165
+ this.lastFrameAtByConv.delete(conversationId);
153
166
  }
154
167
 
155
168
  private wsUrl(conversationId: string): string {
@@ -187,7 +200,7 @@ export class WsSubscription {
187
200
  // ws_connected will arrive as first message
188
201
  });
189
202
 
190
- ws.on('message', (data: Buffer | string) => {
203
+ ws.on('message', (data: any) => {
191
204
  try {
192
205
  // Any server frame counts as activity; keep connection stable and avoid unnecessary pings.
193
206
  const state = this.heartbeatStates.get(conversationId);
@@ -200,18 +213,67 @@ export class WsSubscription {
200
213
  }
201
214
  }
202
215
 
203
- const msg = JSON.parse(data.toString());
204
-
205
- if (msg.type === 'ws_message' && msg.envelope) {
216
+ const rawText =
217
+ typeof data === 'string'
218
+ ? data
219
+ : Buffer.isBuffer(data)
220
+ ? data.toString()
221
+ : Array.isArray(data)
222
+ ? Buffer.concat(data).toString()
223
+ : data instanceof ArrayBuffer
224
+ ? Buffer.from(new Uint8Array(data)).toString()
225
+ : // Fallback: try best-effort stringification
226
+ String(data);
227
+
228
+ this.lastFrameAtByConv.set(conversationId, Date.now());
229
+
230
+ const msg = JSON.parse(rawText);
231
+
232
+ if (msg.type === 'ws_connected') {
233
+ this.opts.onDebug?.({
234
+ event: 'ws_connected',
235
+ conversationId,
236
+ detail: {
237
+ your_did: msg.your_did,
238
+ server_ts_ms: msg.server_ts_ms,
239
+ conversation_id: msg.conversation_id,
240
+ },
241
+ });
242
+ } else if (msg.type === 'ws_message' && msg.envelope) {
206
243
  const env = msg.envelope;
207
- if (env.sender_did !== this.opts.myDid) {
244
+ const ignoreSelf = this.opts.ignoreSelfMessages ?? true;
245
+ if (!ignoreSelf || env.sender_did !== this.opts.myDid) {
208
246
  this.opts.onMessage(env, conversationId);
247
+ } else {
248
+ this.opts.onDebug?.({
249
+ event: 'ws_message_ignored_self',
250
+ conversationId,
251
+ detail: { sender_did: env.sender_did, message_id: env.message_id, seq: env.seq },
252
+ });
209
253
  }
210
254
  } else if (msg.type === 'ws_control' && msg.control) {
211
255
  this.opts.onControl?.(msg.control, conversationId);
256
+ this.opts.onDebug?.({ event: 'ws_control', conversationId, detail: msg.control });
257
+ } else if (msg.type === 'ws_receipt' && msg.receipt) {
258
+ this.opts.onDebug?.({ event: 'ws_receipt', conversationId, detail: msg.receipt });
259
+ } else if (msg?.type) {
260
+ this.opts.onDebug?.({ event: 'ws_unknown_type', conversationId, detail: { type: msg.type } });
261
+ }
262
+ } catch (e: any) {
263
+ // Observability: parse failures are otherwise silent and look like "server never pushes".
264
+ const now = Date.now();
265
+ const last = this.lastParseErrorAtByConv.get(conversationId) ?? 0;
266
+ if (now - last > 30_000) {
267
+ this.lastParseErrorAtByConv.set(conversationId, now);
268
+ this.opts.onDebug?.({
269
+ event: 'ws_parse_error',
270
+ conversationId,
271
+ detail: {
272
+ message: e?.message ?? String(e),
273
+ last_frame_at_ms: this.lastFrameAtByConv.get(conversationId) ?? null,
274
+ },
275
+ });
212
276
  }
213
- } catch {
214
- // ignore parse errors
215
277
  }
216
278
  });
217
279
 
@@ -320,10 +382,15 @@ export class WsSubscription {
320
382
  this.reconnectTimers.set(conversationId, timer);
321
383
  }
322
384
 
385
+ private isSubscribableConversationType(type: string): boolean {
386
+ // Server may return pending_dm alongside dm listings. WS upgrade is supported for any conversation_id.
387
+ return type === 'dm' || type === 'pending_dm' || type === 'channel' || type === 'group';
388
+ }
389
+
323
390
  private async connectAll(): Promise<void> {
324
391
  try {
325
392
  const convos = await this.opts.listConversations();
326
- const subscribable = convos.filter((c) => c.type === 'dm' || c.type === 'channel');
393
+ const subscribable = convos.filter((c) => this.isSubscribableConversationType(c.type));
327
394
  for (const c of subscribable) {
328
395
  void this.connectAsync(c.conversation_id);
329
396
  }
@@ -337,7 +404,7 @@ export class WsSubscription {
337
404
  try {
338
405
  const convos = await this.opts.listConversations();
339
406
  const subscribableIds = new Set(
340
- convos.filter((c) => c.type === 'dm' || c.type === 'channel').map((c) => c.conversation_id),
407
+ convos.filter((c) => this.isSubscribableConversationType(c.type)).map((c) => c.conversation_id),
341
408
  );
342
409
  for (const convId of subscribableIds) {
343
410
  if (!this.stoppedConversations.has(convId) && !this.connections.has(convId)) {