@kybernesis/buzz 0.10.1 → 0.10.2

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/turn.d.ts CHANGED
@@ -82,8 +82,17 @@ export declare function silentTurnReply(failures: readonly TurnFailure[]): strin
82
82
  export declare function describeSilentTurn(failures: readonly TurnFailure[]): string;
83
83
  /** Convert one inbound Buzz message into an eve-compatible turn. */
84
84
  export declare function composeMessage(text: string, attachments: MediaRef[], fetchAttachment: (ref: MediaRef) => Promise<FetchedMedia>, log?: (message: string) => void): Promise<TurnMessage>;
85
+ /**
86
+ * How long reading the backlog of a conversation on record may take before the
87
+ * conversation is treated as gone. An idle session's unread events arrive in
88
+ * well under a second; a session whose durable run died under a runtime
89
+ * upgrade never finishes. On 2026-10-05 a Buzz channel's stored run had failed
90
+ * replay after the eve 0.68 cutover, and every message in that channel sat at
91
+ * "typing" with nothing reaching the agent, for as long as anyone waited.
92
+ */
93
+ export declare const DEFAULT_UNREAD_DRAIN_TIMEOUT_MS = 20000;
85
94
  /** Continue a channel's existing conversation, replacing it only when it is explicitly stale. */
86
- export declare function answerTurn(client: Client, sessions: SessionStore, channel: string, message: TurnMessage, community: string, log?: (message: string) => void, agentSilenceTimeoutMs?: number, agentWorkTimeoutMs?: number): Promise<TurnOutcome>;
95
+ export declare function answerTurn(client: Client, sessions: SessionStore, channel: string, message: TurnMessage, community: string, log?: (message: string) => void, agentSilenceTimeoutMs?: number, agentWorkTimeoutMs?: number, unreadDrainTimeoutMs?: number): Promise<TurnOutcome>;
87
96
  /** What the room is told when eve refuses the turn itself. */
88
97
  /**
89
98
  * The two answers from eve that mean the stored session cannot carry another
package/dist/turn.js CHANGED
@@ -313,12 +313,23 @@ async function sendExisting(responsePromise, streamIndex, timeouts, continueStre
313
313
  watchdog.dispose();
314
314
  }
315
315
  }
316
+ /**
317
+ * How long reading the backlog of a conversation on record may take before the
318
+ * conversation is treated as gone. An idle session's unread events arrive in
319
+ * well under a second; a session whose durable run died under a runtime
320
+ * upgrade never finishes. On 2026-10-05 a Buzz channel's stored run had failed
321
+ * replay after the eve 0.68 cutover, and every message in that channel sat at
322
+ * "typing" with nothing reaching the agent, for as long as anyone waited.
323
+ */
324
+ export const DEFAULT_UNREAD_DRAIN_TIMEOUT_MS = 20_000;
316
325
  /** Continue a channel's existing conversation, replacing it only when it is explicitly stale. */
317
- export async function answerTurn(client, sessions, channel, message, community, log = () => { }, agentSilenceTimeoutMs = DEFAULT_AGENT_SILENCE_TIMEOUT_MS, agentWorkTimeoutMs = DEFAULT_AGENT_WORK_TIMEOUT_MS) {
326
+ export async function answerTurn(client, sessions, channel, message, community, log = () => { }, agentSilenceTimeoutMs = DEFAULT_AGENT_SILENCE_TIMEOUT_MS, agentWorkTimeoutMs = DEFAULT_AGENT_WORK_TIMEOUT_MS, unreadDrainTimeoutMs = DEFAULT_UNREAD_DRAIN_TIMEOUT_MS) {
318
327
  const intervalMs = {
319
328
  silenceMs: validateAgentSilenceTimeoutMs(agentSilenceTimeoutMs),
320
329
  workMs: validateAgentSilenceTimeoutMs(agentWorkTimeoutMs),
321
330
  };
331
+ // Never longer than the silence bound, so a caller that tightened silence tightened this too.
332
+ const drainMs = Math.min(validateAgentSilenceTimeoutMs(unreadDrainTimeoutMs), intervalMs.silenceMs);
322
333
  const existing = sessions.get(community, channel);
323
334
  if (existing) {
324
335
  try {
@@ -330,8 +341,24 @@ export async function answerTurn(client, sessions, channel, message, community,
330
341
  * `send()` opens its response stream at the position the handle already
331
342
  * holds and stops at the FIRST turn boundary it meets. Draining first
332
343
  * repairs any cursor that was left behind by a prior disconnected reader.
344
+ * Bounded on its own, tightly: this read is of a backlog, not of work.
345
+ * A backlog that does not finish is not proof of anything by itself —
346
+ * the run behind it may be dead (eve 0.68 cutover, 2026-10-05: a stored
347
+ * run that failed replay streamed nothing, and the channel sat at
348
+ * "typing" for good) or the transport may be having a moment. So the
349
+ * stall is logged and the send goes ahead: the agent's answer to the
350
+ * send decides. 404 / "no longer active" replaces the session below; a
351
+ * real reply continues it; a transport failure preserves it.
333
352
  */
334
- const unread = await drainUnread((signal) => session.stream({ follow: false, startIndex: existing.streamIndex, signal }), intervalMs);
353
+ let unread = 0;
354
+ try {
355
+ unread = await drainUnread((signal) => session.stream({ follow: false, startIndex: existing.streamIndex, signal }), { silenceMs: drainMs, workMs: drainMs });
356
+ }
357
+ catch (error) {
358
+ if (!(error instanceof AgentSilenceTimeoutError))
359
+ throw error;
360
+ log(`backlog of ${channel.slice(0, 8)} (${existing.id}) did not finish reading in ${drainMs}ms; asking the agent directly`);
361
+ }
335
362
  if (unread > 0) {
336
363
  log(`caught up on ${unread} unread event(s) in ${channel.slice(0, 8)} before answering`);
337
364
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/buzz",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Put an eve agent in a Buzz workspace as a member: per-speaker verified identity resolved through the control plane, self-service identity linking, presence, typing and seen signals.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",