@canonmsg/agent-sdk 3.4.4 → 5.0.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.
@@ -1077,13 +1077,13 @@ export class CanonAgent {
1077
1077
  * where the last-started agent's token wins in multi-agent processes.
1078
1078
  */
1079
1079
  createRuntimeStatePublisher() {
1080
- if (!this.agentId || !this.rtdbHandle)
1080
+ if (!this.agentId)
1081
1081
  return null;
1082
1082
  return createRuntimeStatePublisher({
1083
- rtdb: this.rtdbHandle,
1084
1083
  agentId: this.agentId,
1085
1084
  clientType: this.options.clientType ?? 'generic',
1086
1085
  hostMode: this.options.runtimeControlSurface === 'host',
1086
+ ...(this.rtdbHandle ? { rtdb: this.rtdbHandle } : {}),
1087
1087
  });
1088
1088
  }
1089
1089
  requireRuntimeStatePublisher() {
@@ -1393,11 +1393,7 @@ export class CanonAgent {
1393
1393
  const deleteMessage = (messageId) => this.apiClient.deleteMessage(conversationId, messageId);
1394
1394
  const markAsRead = () => this.apiClient.markAsRead(conversationId);
1395
1395
  const leave = () => this.apiClient.leaveConversation(conversationId);
1396
- // Handler contract stays Promise<void>; the client's toggle result
1397
- // (added/removed + reactions) is a bridge-era addition it doesn't use.
1398
- const react = async (messageId, emoji) => {
1399
- await this.apiClient.react(conversationId, messageId, emoji);
1400
- };
1396
+ const react = (messageId, emoji) => this.apiClient.react(conversationId, messageId, emoji);
1401
1397
  const addMember = (userId) => this.apiClient.addMember(conversationId, userId);
1402
1398
  const removeMember = (userId) => this.apiClient.removeMember(conversationId, userId);
1403
1399
  const sendContextualMessage = (target, text, options) => this.apiClient.sendContextualMessage({
@@ -1959,7 +1955,7 @@ export class CanonAgent {
1959
1955
  await turnOutput.flush();
1960
1956
  },
1961
1957
  clear: async () => {
1962
- await turnOutput.clear().catch(() => { });
1958
+ await turnOutput.clear();
1963
1959
  },
1964
1960
  setTool: async (text) => {
1965
1961
  await writeTurn('tool');
@@ -4,22 +4,15 @@ import { Debouncer } from './debouncer.js';
4
4
  * Wraps @canonmsg/core's CanonStream with SDK-specific features:
5
5
  * - Debouncer integration (message batching)
6
6
  * - Agent context callback
7
- * - REST catch-up when the SSE replay window expires (replay.expired)
7
+ * - Connection/status callbacks
8
8
  */
9
9
  export declare class RealtimeManager {
10
10
  private debouncer;
11
11
  private agentId;
12
- private apiClient;
13
12
  private stream;
14
13
  private running;
15
14
  /** Recent inbound message IDs (`conversationId:messageId`) for cross-flush dedupe. */
16
15
  private readonly recentInboundMessageIds;
17
- /** Latest handled inbound message timestamp per conversation. */
18
- private readonly lastInboundMessageAtByConversation;
19
- /** Lower bound for catch-up in conversations with no in-memory cursor. */
20
- private readonly replaySyncStartedAt;
21
- private replayCatchupInFlight;
22
- private hasConnectedOnce;
23
16
  private lastSseErrorKey;
24
17
  private lastSseErrorAt;
25
18
  private suppressedSseErrorCount;
@@ -34,24 +27,9 @@ export declare class RealtimeManager {
34
27
  private onConnected;
35
28
  private onDisconnected;
36
29
  constructor(apiKey: string, debouncer: Debouncer, agentId: string, streamUrl?: string, apiClient?: CanonClient);
37
- private queueReplayCatchup;
38
30
  private hasSeenInboundMessage;
39
31
  private recordSeenInboundMessage;
40
32
  private pruneRecentInboundMessageIds;
41
- /**
42
- * REST catch-up after `replay.expired`: the stream service evicted our
43
- * cursor, so messages in the gap were silently dropped. Fetch the newest
44
- * page per conversation and feed unseen inbound messages through the normal
45
- * debouncer path (same entry point as SSE delivery, same id dedupe).
46
- *
47
- * Lower bound per conversation: the in-memory last-seen inbound timestamp,
48
- * falling back to this manager's construction time for conversations with
49
- * no prior inbound traffic — anything older predates this process and may
50
- * already have been handled by a previous run. For the same reason the
51
- * catch-up is NOT wired on initial connect: with no durable cursor, a fresh
52
- * process would re-fire turns for messages an earlier run already answered.
53
- */
54
- private runReplayCatchup;
55
33
  private logSseError;
56
34
  setOnAgentContext(cb: (ctx: AgentContext) => void): void;
57
35
  setContactRequestHandlers(handlers: {
package/dist/realtime.js CHANGED
@@ -1,14 +1,6 @@
1
1
  import { CanonStream, } from '@canonmsg/core';
2
- import { shouldDispatchInboundMessage } from './turn-filter.js';
3
2
  const RECENT_INBOUND_TTL_MS = 30 * 60 * 1000;
4
3
  const MAX_RECENT_INBOUND_MESSAGE_IDS = 5000;
5
- /**
6
- * Newest-page bound for the replay-expiry REST catch-up. The SDK has no
7
- * durable per-conversation cursor (everything here is in-memory), so the
8
- * catch-up only inspects the newest page per conversation and relies on the
9
- * id-based dedupe below for anything that overlaps live SSE delivery.
10
- */
11
- const REPLAY_CATCHUP_PAGE_LIMIT = 50;
12
4
  function messageCreatedAtMs(createdAt) {
13
5
  if (!createdAt)
14
6
  return 0;
@@ -19,22 +11,15 @@ function messageCreatedAtMs(createdAt) {
19
11
  * Wraps @canonmsg/core's CanonStream with SDK-specific features:
20
12
  * - Debouncer integration (message batching)
21
13
  * - Agent context callback
22
- * - REST catch-up when the SSE replay window expires (replay.expired)
14
+ * - Connection/status callbacks
23
15
  */
24
16
  export class RealtimeManager {
25
17
  debouncer;
26
18
  agentId;
27
- apiClient;
28
19
  stream;
29
20
  running = false;
30
21
  /** Recent inbound message IDs (`conversationId:messageId`) for cross-flush dedupe. */
31
22
  recentInboundMessageIds = new Map();
32
- /** Latest handled inbound message timestamp per conversation. */
33
- lastInboundMessageAtByConversation = new Map();
34
- /** Lower bound for catch-up in conversations with no in-memory cursor. */
35
- replaySyncStartedAt = Date.now();
36
- replayCatchupInFlight = null;
37
- hasConnectedOnce = false;
38
23
  lastSseErrorKey = null;
39
24
  lastSseErrorAt = 0;
40
25
  suppressedSseErrorCount = 0;
@@ -51,15 +36,14 @@ export class RealtimeManager {
51
36
  constructor(apiKey, debouncer, agentId, streamUrl, apiClient) {
52
37
  this.debouncer = debouncer;
53
38
  this.agentId = agentId;
54
- this.apiClient = apiClient ?? null;
55
39
  this.stream = new CanonStream({
56
40
  apiKey,
57
41
  agentId,
58
42
  streamUrl,
59
43
  handler: {
60
44
  onMessage: (payload) => {
61
- // Cross-flush id dedupe: replay overlap or a concurrent REST
62
- // catch-up must never double-fire a turn for the same message.
45
+ // Cross-flush id dedupe: SSE replay overlap must never double-fire
46
+ // a turn for the same message.
63
47
  if (this.hasSeenInboundMessage(payload.conversationId, payload.message.id)) {
64
48
  return;
65
49
  }
@@ -122,20 +106,13 @@ export class RealtimeManager {
122
106
  this.onConversationUpdated?.(payload);
123
107
  },
124
108
  onConnected: () => {
125
- // Reset backoff is handled internally by CanonStream
126
- const shouldCatchUp = this.hasConnectedOnce;
127
- this.hasConnectedOnce = true;
128
109
  this.onConnected?.();
129
- if (shouldCatchUp) {
130
- this.queueReplayCatchup();
131
- }
132
110
  },
133
111
  onDisconnected: () => {
134
112
  this.onDisconnected?.();
135
113
  },
136
114
  onReplayExpired: (payload) => {
137
- console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''} catching up over REST`);
138
- this.queueReplayCatchup();
115
+ console.error(`[canon-sdk] SSE replay window expired${payload.lastAvailableId ? ` (oldest available event: ${payload.lastAvailableId})` : ''}; missed history is available via explicit REST fetch`);
139
116
  },
140
117
  onError: (err) => {
141
118
  this.logSseError(err);
@@ -143,22 +120,13 @@ export class RealtimeManager {
143
120
  },
144
121
  });
145
122
  }
146
- queueReplayCatchup() {
147
- this.replayCatchupInFlight ??= this.runReplayCatchup().finally(() => {
148
- this.replayCatchupInFlight = null;
149
- });
150
- }
151
123
  hasSeenInboundMessage(conversationId, messageId) {
152
124
  return this.recentInboundMessageIds.has(`${conversationId}:${messageId}`);
153
125
  }
154
126
  recordSeenInboundMessage(conversationId, messageId, createdAtMs) {
155
127
  const now = Date.now();
156
128
  this.recentInboundMessageIds.set(`${conversationId}:${messageId}`, now);
157
- const effectiveTimestamp = createdAtMs > 0 ? createdAtMs : now;
158
- const previous = this.lastInboundMessageAtByConversation.get(conversationId) ?? 0;
159
- if (effectiveTimestamp > previous) {
160
- this.lastInboundMessageAtByConversation.set(conversationId, effectiveTimestamp);
161
- }
129
+ void createdAtMs;
162
130
  this.pruneRecentInboundMessageIds(now);
163
131
  }
164
132
  pruneRecentInboundMessageIds(now = Date.now()) {
@@ -175,75 +143,6 @@ export class RealtimeManager {
175
143
  this.recentInboundMessageIds.delete(oldestKey);
176
144
  }
177
145
  }
178
- /**
179
- * REST catch-up after `replay.expired`: the stream service evicted our
180
- * cursor, so messages in the gap were silently dropped. Fetch the newest
181
- * page per conversation and feed unseen inbound messages through the normal
182
- * debouncer path (same entry point as SSE delivery, same id dedupe).
183
- *
184
- * Lower bound per conversation: the in-memory last-seen inbound timestamp,
185
- * falling back to this manager's construction time for conversations with
186
- * no prior inbound traffic — anything older predates this process and may
187
- * already have been handled by a previous run. For the same reason the
188
- * catch-up is NOT wired on initial connect: with no durable cursor, a fresh
189
- * process would re-fire turns for messages an earlier run already answered.
190
- */
191
- async runReplayCatchup() {
192
- const apiClient = this.apiClient;
193
- if (!apiClient) {
194
- console.error('[canon-sdk] Replay catch-up skipped — no API client available');
195
- return;
196
- }
197
- try {
198
- const conversations = await apiClient.getConversations();
199
- let recovered = 0;
200
- await Promise.all(conversations.map(async (conversation) => {
201
- try {
202
- const page = await apiClient.getMessagesPage(conversation.id, REPLAY_CATCHUP_PAGE_LIMIT);
203
- const lowerBoundMs = this.lastInboundMessageAtByConversation.get(conversation.id)
204
- ?? this.replaySyncStartedAt;
205
- const candidates = [...(page.messages ?? [])]
206
- .filter((message) => !message.deleted)
207
- .sort((a, b) => messageCreatedAtMs(a.createdAt) - messageCreatedAtMs(b.createdAt));
208
- for (const message of candidates) {
209
- if (!this.running)
210
- return;
211
- if (message.senderId === this.agentId)
212
- continue;
213
- const createdAtMs = messageCreatedAtMs(message.createdAt);
214
- // Use a strict lower bound of `< lowerBoundMs` (not `<=`): a
215
- // gap-dropped message can share the same createdAt millisecond as
216
- // the last message seen over SSE (server timestamps collide under
217
- // bursts — exactly the scenario catch-up targets). The id-dedupe on
218
- // the next line suppresses the already-delivered boundary message,
219
- // so excluding by `<=` would only drop never-seen same-ms peers.
220
- if (!createdAtMs || createdAtMs < lowerBoundMs)
221
- continue;
222
- if (this.hasSeenInboundMessage(conversation.id, message.id))
223
- continue;
224
- this.recordSeenInboundMessage(conversation.id, message.id, createdAtMs);
225
- const dispatch = await shouldDispatchInboundMessage(conversation.id, this.agentId, message, {
226
- conversationType: conversation.type,
227
- behavior: page.behavior ?? null,
228
- });
229
- if (!dispatch)
230
- continue;
231
- this.debouncer.add(conversation.id, message, null);
232
- recovered += 1;
233
- }
234
- }
235
- catch (err) {
236
- console.error(`[canon-sdk] Replay catch-up failed for ${conversation.id}:`, err instanceof Error ? err.message : err);
237
- }
238
- }));
239
- if (recovered > 0) {
240
- console.error(`[canon-sdk] Replay catch-up recovered ${recovered} missed message(s)`);
241
- }
242
- }
243
- catch (err) {
244
- console.error('[canon-sdk] Replay catch-up failed:', err instanceof Error ? err.message : err);
245
- }
246
- }
247
146
  logSseError(err) {
248
147
  const code = err.code;
249
148
  const key = `${typeof code === 'string' ? code : 'generic'}:${err.message}`;
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@canonmsg/agent-sdk",
3
- "version": "3.4.4",
3
+ "version": "5.0.0",
4
4
  "description": "Canon Agent SDK — build AI agents that participate in Canon conversations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
12
  },
13
13
  "./media": {
14
- "types": "./dist/media.d.ts",
15
- "import": "./dist/media.js"
14
+ "import": "./dist/media.js",
15
+ "types": "./dist/media.d.ts"
16
16
  }
17
17
  },
18
18
  "files": [
@@ -28,7 +28,7 @@
28
28
  "node": ">=18.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@canonmsg/core": "^3.2.0"
31
+ "@canonmsg/core": "^4.0.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"