osborn 0.9.113 → 0.9.114

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.
Files changed (2) hide show
  1. package/dist/index.js +50 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -192,6 +192,11 @@ const livekitState = {
192
192
  // connect-with-retry loop) so the module-level HTTP server can drive them.
193
193
  let intentionalLeave = false;
194
194
  let connectRoomHook = null;
195
+ // Mints a LISTEN-ONLY LiveKit token for the meeting canvas so it can join the
196
+ // agent's room and play the agent's REAL TTS audio track — bit-identical to
197
+ // the browser voice experience (2026-08-01 quality architecture; replaces the
198
+ // synth-file chain when connected). Set from main() where the room name lives.
199
+ let canvasTokenHook = null;
195
200
  let leaveRoomHook = null;
196
201
  // Hook for the bug-reporter skill. The /report-bug HTTP endpoint validates the
197
202
  // payload + generates the reportId in the module-level handler, then delegates
@@ -342,6 +347,18 @@ function startApiServer(workingDir, port) {
342
347
  }
343
348
  return;
344
349
  }
350
+ if (req.method === 'GET' && url.pathname === '/canvas-token') {
351
+ // Listen-only LiveKit credentials for the meeting canvas (see hook doc).
352
+ const out = canvasTokenHook ? await canvasTokenHook() : null;
353
+ if (!out) {
354
+ res.writeHead(503, { 'Content-Type': 'application/json' });
355
+ res.end(JSON.stringify({ error: 'no active room' }));
356
+ return;
357
+ }
358
+ res.writeHead(200, { 'Content-Type': 'application/json' });
359
+ res.end(JSON.stringify(out));
360
+ return;
361
+ }
345
362
  if (req.method === 'GET' && url.pathname === '/skills') {
346
363
  // Installed skills — same list the chat's get_skills data-channel message
347
364
  // returns, exposed over HTTP so the DASHBOARD (no LiveKit connection) can
@@ -1715,6 +1732,10 @@ async function main() {
1715
1732
  // REDIRECTED into the meeting canvas instead of suppressed — the regular
1716
1733
  // voice pipeline reused with the meeting as the sink (latency fix).
1717
1734
  let meetingAddressedUntil = 0;
1735
+ // True while the meeting-canvas page is connected to the LiveKit room as a
1736
+ // listener (browser-parity audio path). When set, meeting replies use the
1737
+ // NORMAL session.say pipeline — no suppression, no redirect, no synth chain.
1738
+ let meetingCanvasInRoom = false;
1718
1739
  // Distinct human speakers seen in the current meeting. In a 1:1 (one human
1719
1740
  // + the bot) EVERY utterance is addressed to the bot — no name needed
1720
1741
  // (2026-08-01: "can you open carfax..." got observer-suppressed because
@@ -2632,6 +2653,16 @@ async function main() {
2632
2653
  // re-captures in the same room → feedback). Set by PipelineDirectLLM.chat()
2633
2654
  // when the turn is a [MEETING —] chunk. Normal user turns are unaffected.
2634
2655
  if (directLLM.suppressMeetingTTS) {
2656
+ // BROWSER-PARITY PATH: canvas is in the LiveKit room → the normal
2657
+ // session.say audio reaches the meeting through it. Fall through to
2658
+ // the regular pipeline for addressed turns (identical audio to the
2659
+ // browser experience); observer turns still stay silent.
2660
+ if (meetingCanvasInRoom && activeMeetingBotId && Date.now() < meetingAddressedUntil) {
2661
+ console.log(`🔊🎼 meeting reply via NATIVE session.say (canvas relays): "${data.text.slice(0, 50)}"`);
2662
+ markMeetingSpeaking(data.text);
2663
+ // no return — normal say proceeds below
2664
+ }
2665
+ else
2635
2666
  // ADDRESSED turn → REDIRECT the normal streaming reply into the meeting
2636
2667
  // (reuse of the regular speak path; no Bash roundtrip). Silent-observer
2637
2668
  // turns stay suppressed as before.
@@ -3578,6 +3609,14 @@ async function main() {
3578
3609
  // connect callback, not via events).
3579
3610
  participantConnectedHandler = async (participant) => {
3580
3611
  console.log(`\n👤 User joined: ${participant.identity}`);
3612
+ // The meeting canvas joining as a listener = browser-parity audio path is
3613
+ // LIVE: the agent's normal session.say plays through the canvas into the
3614
+ // meeting, so tts_say suppression/redirect must stand down.
3615
+ if (participant.identity === 'meeting-canvas') {
3616
+ meetingCanvasInRoom = true;
3617
+ console.log('📽️🔊 meeting-canvas joined the LiveKit room — native session.say relays to the meeting');
3618
+ return; // not a user; skip session setup for it
3619
+ }
3581
3620
  // A user (re)arriving cancels the meeting leave-grace — the bot stays.
3582
3621
  cancelMeetingLeaveGrace();
3583
3622
  // A user is present — cancel any pending agent-side "alone" leave.
@@ -5481,6 +5520,17 @@ async function main() {
5481
5520
  // { roomName } (which connectRoomHook resolves to). During rollout, the legacy
5482
5521
  // GET /room-code endpoint keeps returning the last-created room NAME so old
5483
5522
  // frontends still function.
5523
+ canvasTokenHook = async () => {
5524
+ if (!activeRoomName)
5525
+ return null;
5526
+ const t = new AccessToken(apiKey, apiSecret, {
5527
+ identity: 'meeting-canvas',
5528
+ name: 'Meeting Canvas',
5529
+ metadata: JSON.stringify({ type: 'canvas' }),
5530
+ });
5531
+ t.addGrant({ roomJoin: true, room: activeRoomName, canPublish: false, canSubscribe: true, canPublishData: false });
5532
+ return { token: await t.toJwt(), url: process.env.LIVEKIT_URL || '', room: activeRoomName };
5533
+ };
5484
5534
  connectRoomHook = async () => {
5485
5535
  intentionalLeave = false;
5486
5536
  cancelIdleExitTimer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.113",
3
+ "version": "0.9.114",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {