osborn 0.9.84 → 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);
@@ -4624,7 +4630,11 @@ async function main() {
4624
4630
  // pipeline at all. Recall captures the meeting audio internally
4625
4631
  // and we pull the transcript via its REST API every ~30s.
4626
4632
  await sendToFrontend({ type: 'meeting_joining', message: 'Osborn is joining your meeting...' });
4627
- const botId = await recallJoin.joinMeeting(meetingUrl, webhookBase);
4633
+ // 0.9.85: optional castUrl — a webpage to display as the bot's
4634
+ // camera (live feed / seeded site). Passed from the frontend or
4635
+ // env OSBORN_MEETING_CAST_URL for testing.
4636
+ const castUrl = data.castUrl || process.env.OSBORN_MEETING_CAST_URL || undefined;
4637
+ const botId = await recallJoin.joinMeeting(meetingUrl, webhookBase, { castUrl });
4628
4638
  const sessionId = currentLLM?.sessionId || currentResumeSessionId || 'default';
4629
4639
  recallJoin.registerBot(botId, sessionId);
4630
4640
  activeMeetingBotId = botId;
@@ -81,6 +81,7 @@ export declare class RecallClient extends EventEmitter {
81
81
  */
82
82
  joinMeeting(meetingUrl: string, webhookBaseUrl: string, opts?: {
83
83
  botName?: string;
84
+ castUrl?: string;
84
85
  }): Promise<string>;
85
86
  /**
86
87
  * Fetch the bot's current transcript. Returns an array of "transcript turns"
@@ -23,6 +23,19 @@ export class RecallClient extends EventEmitter {
23
23
  */
24
24
  async joinMeeting(meetingUrl, webhookBaseUrl, opts) {
25
25
  const botName = opts?.botName ?? 'Osborn';
26
+ // 0.9.85: optional CAST — display a webpage as the bot's camera in the
27
+ // meeting (Recall output_media, the piece the v0.9.44 rewrite removed).
28
+ // Point castUrl at the session-engine live-feed viewer, a seeded site
29
+ // (e.g. voice-native's LinkedIn), or a research/output page. This is the
30
+ // "the client sees content" visual side of the copilot. Left off by
31
+ // default (silent, invisible observer) — only casts when castUrl is set.
32
+ const cast = opts?.castUrl && /^https?:\/\//.test(opts.castUrl)
33
+ ? { output_media: { camera: { kind: 'webpage', config: { url: opts.castUrl } } } }
34
+ : {};
35
+ if (opts?.castUrl && !Object.keys(cast).length)
36
+ console.log(`⚠️ castUrl ignored (not http(s)): ${opts.castUrl}`);
37
+ else if (Object.keys(cast).length)
38
+ console.log(`📺 Meeting cast enabled — bot camera = ${opts.castUrl}`);
26
39
  // 0.9.84: LIVE transcript via realtime_endpoints. The v0.9.44 rewrite
27
40
  // removed this and left only 30s polling of the batch download_url — but
28
41
  // that URL is a POST-PROCESSING artifact (empty until the meeting ends),
@@ -58,6 +71,7 @@ export class RecallClient extends EventEmitter {
58
71
  body: JSON.stringify({
59
72
  meeting_url: meetingUrl,
60
73
  bot_name: botName,
74
+ ...cast,
61
75
  recording_config: {
62
76
  transcript: {
63
77
  provider: {
@@ -135,9 +149,19 @@ export class RecallClient extends EventEmitter {
135
149
  const bot = (await res.json());
136
150
  return bot.status_changes?.at(-1)?.code ?? 'unknown';
137
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();
138
156
  handleWebhook(payload) {
139
- // Only process final transcripts (transcript.data), skip partials
140
- 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)
141
165
  return;
142
166
  const words = payload.data?.data?.words ?? [];
143
167
  const text = words.map(w => w.text).join(' ').trim();
@@ -145,7 +169,15 @@ export class RecallClient extends EventEmitter {
145
169
  return;
146
170
  const speaker = payload.data?.data?.participant?.name ?? 'Unknown';
147
171
  const botId = payload.data?.bot?.id ?? 'unknown';
148
- 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 });
149
181
  }
150
182
  registerBot(botId, sessionId) {
151
183
  this.#activeBots.set(botId, sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.84",
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": {