osborn 0.9.30 → 0.9.32

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.
@@ -17,12 +17,17 @@ Do not batch — capture as they happen.
17
17
  </what-to-do>
18
18
 
19
19
  <trigger-phrases>
20
- - "grill me"
20
+ This skill is a META operation — about building a model of the USER THEMSELVES,
21
+ not about any subject matter in the current conversation. The trigger phrases below
22
+ are intentionally specific so they cannot be confused with domain requests in any session.
23
+
24
+ - "update user context"
21
25
  - "learn my language"
22
- - "update my context"
26
+ - "start context interview"
27
+ - "grill me on my language"
23
28
  - "learn how I talk"
24
29
  - "standardise my language"
25
- - "update user context"
30
+ - "update my context"
26
31
  </trigger-phrases>
27
32
 
28
33
  <supporting-info>
package/dist/index.js CHANGED
@@ -1111,6 +1111,7 @@ async function main() {
1111
1111
  // Empty string = anonymous / unauthenticated; uploads fall back to a
1112
1112
  // session-only path (no user prefix).
1113
1113
  let currentUserId = '';
1114
+ let activeMeetingBotId = null; // Recall.ai bot ID if in a meeting
1114
1115
  // Track the active resume session ID across scopes (ParticipantConnected + DataReceived)
1115
1116
  // Updated by resume_session, session_selected, continue_session, switch_session handlers
1116
1117
  let currentResumeSessionId;
@@ -3099,6 +3100,15 @@ async function main() {
3099
3100
  currentLLM = null;
3100
3101
  clearFastBrainSession();
3101
3102
  clearPipelineFastBrainSession();
3103
+ // Auto-leave any active meeting bot when user disconnects from the room
3104
+ if (activeMeetingBotId) {
3105
+ const recallDisconnect = getRecallClient();
3106
+ if (recallDisconnect) {
3107
+ console.log(`🤝 Auto-leaving meeting (bot ${activeMeetingBotId}) — user disconnected from room`);
3108
+ recallDisconnect.leaveMeeting(activeMeetingBotId).catch(() => { });
3109
+ activeMeetingBotId = null;
3110
+ }
3111
+ }
3102
3112
  console.log('⏳ Waiting for new user...\n');
3103
3113
  });
3104
3114
  room.on(RoomEvent.DataReceived, async (payload, participant, kind, topic) => {
@@ -3678,6 +3688,7 @@ async function main() {
3678
3688
  const botId = await recallJoin.joinMeeting(meetingUrl, webhookBase);
3679
3689
  const sessionId = currentLLM?.sessionId || currentResumeSessionId || 'default';
3680
3690
  recallJoin.registerBot(botId, sessionId);
3691
+ activeMeetingBotId = botId;
3681
3692
  await sendToFrontend({ type: 'meeting_joined', botId, message: 'Osborn has joined the meeting' });
3682
3693
  }
3683
3694
  catch (err) {
@@ -3693,6 +3704,7 @@ async function main() {
3693
3704
  if (recallLeave && botId) {
3694
3705
  try {
3695
3706
  await recallLeave.leaveMeeting(botId);
3707
+ activeMeetingBotId = null;
3696
3708
  await sendToFrontend({ type: 'meeting_left', botId });
3697
3709
  }
3698
3710
  catch (err) {
@@ -4,18 +4,33 @@ export interface RecallBot {
4
4
  meeting_url: string;
5
5
  status: string;
6
6
  }
7
- export interface TranscriptWord {
8
- text: string;
9
- start_time: number;
10
- end_time: number;
11
- }
12
7
  export interface TranscriptPayload {
13
- bot_id: string;
14
- transcript: {
15
- speaker: string;
16
- words: TranscriptWord[];
17
- is_final: boolean;
18
- language?: string;
8
+ event: string;
9
+ data: {
10
+ data: {
11
+ words: Array<{
12
+ text: string;
13
+ start_timestamp?: {
14
+ relative?: number;
15
+ };
16
+ end_timestamp?: {
17
+ relative?: number;
18
+ };
19
+ }>;
20
+ language_code?: string;
21
+ participant?: {
22
+ id: number;
23
+ name: string;
24
+ is_host?: boolean;
25
+ platform?: string;
26
+ };
27
+ };
28
+ bot?: {
29
+ id: string;
30
+ };
31
+ recording?: {
32
+ id: string;
33
+ };
19
34
  };
20
35
  }
21
36
  export declare class RecallClient extends EventEmitter {
@@ -9,6 +9,17 @@ export class RecallClient extends EventEmitter {
9
9
  this.#apiKey = apiKey;
10
10
  }
11
11
  async joinMeeting(meetingUrl, webhookBaseUrl, botName = 'Osborn') {
12
+ // Authoritative structure per https://docs.recall.ai/reference/bot_create
13
+ // and https://docs.recall.ai/docs/real-time-transcription:
14
+ //
15
+ // recording_config.transcript.provider — transcription provider config
16
+ // recording_config.realtime_endpoints — webhook/websocket delivery
17
+ //
18
+ // IMPORTANT:
19
+ // - Field is `realtime_endpoints` (NOT `real_time_endpoints`)
20
+ // - `url` and `events` are flat on the endpoint object (NOT nested under `config`)
21
+ // - `transcription_options` does NOT exist — use `transcript.provider`
22
+ // - Both transcript.provider AND realtime_endpoints must be set, or no events delivered
12
23
  const res = await fetch(`${RECALL_BASE_URL}/bot`, {
13
24
  method: 'POST',
14
25
  headers: {
@@ -19,24 +30,26 @@ export class RecallClient extends EventEmitter {
19
30
  meeting_url: meetingUrl,
20
31
  bot_name: botName,
21
32
  recording_config: {
22
- // Field names must match Recall API exactly (no underscore in realtime_endpoints).
23
- // real_time_endpoints was silently ignored — API uses realtime_endpoints.
33
+ transcript: {
34
+ provider: {
35
+ // recallai_streaming is built-in — no external API key needed,
36
+ // low-latency, works across all meeting platforms.
37
+ recallai_streaming: {
38
+ mode: 'prioritize_low_latency',
39
+ language_code: 'en',
40
+ },
41
+ },
42
+ },
24
43
  realtime_endpoints: [{
25
44
  type: 'webhook',
26
- config: {
27
- url: `${webhookBaseUrl}/webhook/recall`,
28
- events: ['transcript.data'],
29
- },
45
+ url: `${webhookBaseUrl}/webhook/recall`,
46
+ events: ['transcript.data'],
30
47
  }],
31
- transcription_options: {
32
- provider: 'assembly_ai',
33
- mode: 'prioritize_low_latency',
34
- },
35
48
  },
36
49
  output_media: {
37
50
  camera: {
38
- // Recall API expects `kind` (not `type`); the wrong key arrives as null and
39
- // gets rejected as "Invalid choice null. Expected 'webpage' or 'default'."
51
+ // `kind` (not `type`) confirmed from prior debugging.
52
+ // Output webpage plays TTS audio so meeting participants can hear the agent.
40
53
  kind: 'webpage',
41
54
  config: {
42
55
  url: `${webhookBaseUrl}/meeting-output`,
@@ -69,16 +82,16 @@ export class RecallClient extends EventEmitter {
69
82
  return bot.status_changes?.at(-1)?.code ?? 'unknown';
70
83
  }
71
84
  handleWebhook(payload) {
72
- if (!payload.transcript?.is_final)
85
+ // Only process final transcripts (transcript.data), skip partials
86
+ if (payload.event !== 'transcript.data')
73
87
  return;
74
- const text = payload.transcript.words.map(w => w.text).join(' ').trim();
88
+ const words = payload.data?.data?.words ?? [];
89
+ const text = words.map(w => w.text).join(' ').trim();
75
90
  if (!text)
76
91
  return;
77
- this.emit('transcript', {
78
- botId: payload.bot_id,
79
- speaker: payload.transcript.speaker,
80
- text,
81
- });
92
+ const speaker = payload.data?.data?.participant?.name ?? 'Unknown';
93
+ const botId = payload.data?.bot?.id ?? 'unknown';
94
+ this.emit('transcript', { botId, speaker, text });
82
95
  }
83
96
  registerBot(botId, sessionId) {
84
97
  this.#activeBots.set(botId, sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.30",
3
+ "version": "0.9.32",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {