osborn 0.9.85 → 0.9.86

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.js CHANGED
@@ -303,6 +303,12 @@ function startApiServer(workingDir, port) {
303
303
  req.on('end', () => {
304
304
  try {
305
305
  const payload = JSON.parse(body);
306
+ // 0.9.86: log every receipt so we can SEE what Recall streams
307
+ // (event type + word count) — the handler was silent, making it
308
+ // impossible to tell whether webhooks arrive or are just filtered.
309
+ const evt = payload?.event ?? 'unknown';
310
+ const wc = (payload?.data?.data?.words ?? []).length;
311
+ console.log(`📨 Recall webhook: event=${evt} words=${wc}`);
306
312
  const recall = getRecallClient();
307
313
  if (recall)
308
314
  recall.handleWebhook(payload);
@@ -149,9 +149,19 @@ export class RecallClient extends EventEmitter {
149
149
  const bot = (await res.json());
150
150
  return bot.status_changes?.at(-1)?.code ?? 'unknown';
151
151
  }
152
+ // 0.9.86: track the last text we emitted per speaker so partials don't
153
+ // spam the LLM with every incremental word — only emit when the utterance
154
+ // grows meaningfully or finalizes.
155
+ #lastEmitted = new Map();
152
156
  handleWebhook(payload) {
153
- // Only process final transcripts (transcript.data), skip partials
154
- if (payload.event !== 'transcript.data')
157
+ // Accept BOTH finals (transcript.data) AND partials (transcript.partial_data).
158
+ // recallai_streaming in prioritize_low_latency mode emits partials DURING
159
+ // the call and finals lag — ignoring partials (the old behavior) meant no
160
+ // live transcript at all (confirmed 2026-07-29: 0 webhook-processed turns
161
+ // mid-call). We emit partials for liveness, deduped against the last text.
162
+ const isFinal = payload.event === 'transcript.data';
163
+ const isPartial = payload.event === 'transcript.partial_data';
164
+ if (!isFinal && !isPartial)
155
165
  return;
156
166
  const words = payload.data?.data?.words ?? [];
157
167
  const text = words.map(w => w.text).join(' ').trim();
@@ -159,7 +169,15 @@ export class RecallClient extends EventEmitter {
159
169
  return;
160
170
  const speaker = payload.data?.data?.participant?.name ?? 'Unknown';
161
171
  const botId = payload.data?.bot?.id ?? 'unknown';
162
- this.emit('transcript', { botId, speaker, text });
172
+ const key = `${botId}:${speaker}`;
173
+ // Dedup: skip a partial that just repeats/prefixes what we already sent.
174
+ const prev = this.#lastEmitted.get(key) ?? '';
175
+ if (!isFinal && (text === prev || (prev && text.startsWith(prev) && text.length - prev.length < 8)))
176
+ return;
177
+ this.#lastEmitted.set(key, text);
178
+ if (isFinal)
179
+ this.#lastEmitted.delete(key); // reset for the next utterance
180
+ this.emit('transcript', { botId, speaker, text, partial: !isFinal });
163
181
  }
164
182
  registerBot(botId, sessionId) {
165
183
  this.#activeBots.set(botId, sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.85",
3
+ "version": "0.9.86",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {