osborn 0.9.90 → 0.9.91

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.
@@ -39,6 +39,35 @@ Task(
39
39
 
40
40
  Give the writer everything it needs in the prompt: the session-workspace path, the bot ID, the `us-west-2.recall.ai` endpoint rule, and the `meeting-todos.md` structure. The writer runs the curl/jq/Write steps itself. For research, delegate to the `researcher` sub-agent the same way.
41
41
 
42
+ ## How to SPEAK INTO the meeting (out of silent mode)
43
+
44
+ You can talk directly into the Google Meet / Zoom — your words play as the bot's
45
+ voice. The bot casts a "meeting canvas" webpage as its camera+mic; POSTing to
46
+ your own HTTP API pushes speech (and visuals) to it, and Recall pipes the
47
+ canvas audio into the call. This is ONE Bash call, within budget.
48
+
49
+ **Speak into the meeting:**
50
+ ```bash
51
+ curl -sS -X POST http://localhost:8741/canvas -H 'Content-Type: application/json' \
52
+ -d '{"kind":"say","text":"YOUR WORDS HERE"}'
53
+ ```
54
+ (Port is `OSBORN_API_PORT`, default 8741.)
55
+
56
+ **Show a visual on the bot's camera** (notes, a link, a title):
57
+ ```bash
58
+ curl -sS -X POST http://localhost:8741/canvas -H 'Content-Type: application/json' \
59
+ -d '{"kind":"show","mode":"notes","title":"...","items":["...","..."]}'
60
+ ```
61
+ `mode` = `idle` | `notes` (title+items) | `link` (url) | `web` (iframe url) | `text` (title+text).
62
+
63
+ **When to speak into the meeting:** By DEFAULT stay silent (observer) for
64
+ `[MEETING — *]:` chunks — take notes, don't interrupt. Speak into the meeting
65
+ ONLY when: (a) the voice-native user explicitly tells you to say something to the
66
+ meeting / "tell them X" / "answer that", or (b) you're directly addressed by name
67
+ in the meeting and the user has enabled active mode. When you do speak, keep it
68
+ short and let the room continue. This is the toggle between silent-observer and
69
+ active-participant.
70
+
42
71
  ## How to behave (auto-tagged chunks)
43
72
 
44
73
  For every `[MEETING — *]:` message:
@@ -164,7 +164,14 @@ PKG_SKILLS_DIR="/usr/local/lib/node_modules/osborn/.claude/skills"
164
164
  SEED_VERSION_FILE="${HOME_SKILLS_DIR}/.seed-version"
165
165
  mkdir -p "$HOME_SKILLS_DIR"
166
166
  CURRENT_SEED_VERSION=$(cat "$SEED_VERSION_FILE" 2>/dev/null | tr -d '[:space:]' || echo "")
167
- IMAGE_SEED_VERSION="${OSBORN_IMAGE_VERSION:-latest}"
167
+ # Seed version = the INSTALLED package's own version (reliable on every boot),
168
+ # NOT the OSBORN_IMAGE_VERSION env — that env isn't threaded through
169
+ # `fly machine update`, so it went stale and left the marker permanently pinned
170
+ # at 0.9.48, meaning the refresh below never fired and new-version skills never
171
+ # reached the load path. Reading package.json makes CURRENT != IMAGE trip on a
172
+ # real version bump. Falls back to the env / "latest" if the package is unreadable.
173
+ IMAGE_SEED_VERSION=$(grep -m1 '"version"' /usr/local/lib/node_modules/osborn/package.json 2>/dev/null | sed 's/.*"version"[^"]*"\([^"]*\)".*/\1/')
174
+ [ -z "$IMAGE_SEED_VERSION" ] && IMAGE_SEED_VERSION="${OSBORN_IMAGE_VERSION:-latest}"
168
175
  if [ -d "$PKG_SKILLS_DIR" ]; then
169
176
  REFRESHED=0
170
177
  SEEDED=0
package/dist/index.js CHANGED
@@ -1556,6 +1556,36 @@ async function main() {
1556
1556
  let currentUserId = '';
1557
1557
  let activeMeetingBotId = null; // Recall.ai bot ID if in a meeting
1558
1558
  let activeMeetingPoller = null; // Transcript poller bound to that bot
1559
+ // LIVE meeting transcript → LLM (buffered webhook finals). See recall.on('transcript').
1560
+ const meetingTranscriptBuffer = [];
1561
+ let meetingFlushTimer = null;
1562
+ const startMeetingFlush = (botId) => {
1563
+ stopMeetingFlush();
1564
+ console.log(`📓 Meeting transcript flush timer started (bot ${botId}, 20s)`);
1565
+ meetingFlushTimer = setInterval(() => {
1566
+ if (!meetingTranscriptBuffer.length || !currentLLM)
1567
+ return;
1568
+ const turns = meetingTranscriptBuffer.splice(0); // drain
1569
+ const tagged = `[MEETING — ${botId}]:\n${turns.join('\n')}`;
1570
+ try {
1571
+ const ctx = new llm.ChatContext();
1572
+ ctx.addMessage({ role: 'user', content: tagged });
1573
+ currentLLM.chat({ chatCtx: ctx });
1574
+ console.log(`📓 Flushed ${turns.length} meeting turn(s) to LLM`);
1575
+ }
1576
+ catch (err) {
1577
+ console.warn(`⚠️ Meeting flush failed: ${err.message}`);
1578
+ }
1579
+ }, 20_000);
1580
+ };
1581
+ const stopMeetingFlush = () => {
1582
+ if (meetingFlushTimer) {
1583
+ clearInterval(meetingFlushTimer);
1584
+ meetingFlushTimer = null;
1585
+ console.log('📓 Meeting flush timer stopped');
1586
+ }
1587
+ meetingTranscriptBuffer.length = 0;
1588
+ };
1559
1589
  // Track the active resume session ID across scopes (ParticipantConnected + DataReceived)
1560
1590
  // Updated by resume_session, session_selected, continue_session, switch_session handlers
1561
1591
  let currentResumeSessionId;
@@ -1593,26 +1623,18 @@ async function main() {
1593
1623
  // "what was said in the meeting" display, separate from the LLM input path).
1594
1624
  const recall = getRecallClient();
1595
1625
  if (recall) {
1596
- console.log('🎥 Recall.ai client initialized (webhook STT receiver — LLM forwarding disabled, see meeting-bot Phase 2)');
1597
- recall.on('transcript', ({ botId, speaker, text }) => {
1598
- console.log(`📝 Meeting transcript [${speaker}]: ${text}`);
1599
- // INTENTIONALLY DISABLED — see comment above. Audio path is now LiveKit
1600
- // → meeting-bot page publishes meeting audio → agent STT processes it.
1601
- // The line below is preserved as a reference for future re-enablement
1602
- // (e.g. as a display-only feature, NOT as LLM input).
1603
- //
1604
- // if (currentLLM && currentSession) {
1605
- // const meetingText = `[Meeting — ${speaker}]: ${text}`
1606
- // try {
1607
- // if (currentVoiceMode === 'pipeline' || currentVoiceMode === 'direct') {
1608
- // const chatCtx = new llm.ChatContext()
1609
- // chatCtx.addMessage({ role: 'user', content: meetingText })
1610
- // ;(currentLLM as any).chat({ chatCtx })
1611
- // }
1612
- // } catch (err) {
1613
- // console.error('❌ Failed to route meeting transcript:', err)
1614
- // }
1615
- // }
1626
+ console.log('🎥 Recall.ai client initialized — LIVE webhook finals buffered → LLM (batch poller download_url is empty mid-call, so the webhook is the only live transcript source)');
1627
+ // LIVE meeting transcript → LLM. The realtime webhook (handleWebhook) emits
1628
+ // every partial + final. We buffer FINALS (partials are too noisy) and a
1629
+ // flush timer (started on join_meeting) batches them to currentLLM.chat()
1630
+ // as [MEETING — botId]: every ~20s — so the agent actually sees the meeting
1631
+ // and can take notes / delegate to the writer. Previously this was DISABLED
1632
+ // and the poller (batch endpoint) was the only LLM path — but that endpoint
1633
+ // is empty until the meeting ENDS, so mid-call the LLM saw nothing.
1634
+ recall.on('transcript', ({ botId, speaker, text, partial }) => {
1635
+ console.log(`📝 Meeting transcript [${speaker}]${partial ? ' (partial)' : ''}: ${text}`);
1636
+ if (!partial && text.trim())
1637
+ meetingTranscriptBuffer.push(`${speaker}: ${text.trim()}`);
1616
1638
  });
1617
1639
  }
1618
1640
  // ============================================================
@@ -4101,6 +4123,7 @@ async function main() {
4101
4123
  clearFastBrainSession();
4102
4124
  clearPipelineFastBrainSession();
4103
4125
  // Auto-leave any active meeting bot when user disconnects from the room
4126
+ stopMeetingFlush();
4104
4127
  if (activeMeetingPoller) {
4105
4128
  activeMeetingPoller.stop();
4106
4129
  activeMeetingPoller = null;
@@ -4786,6 +4809,9 @@ async function main() {
4786
4809
  },
4787
4810
  });
4788
4811
  activeMeetingPoller.start();
4812
+ // LIVE path: buffer webhook finals + flush to the LLM every 20s.
4813
+ // (The poller above only lands data after the meeting ENDS.)
4814
+ startMeetingFlush(botId);
4789
4815
  }
4790
4816
  catch (err) {
4791
4817
  console.error('❌ Recall.ai join error:', err);
@@ -4799,8 +4825,9 @@ async function main() {
4799
4825
  const recallLeave = getRecallClient();
4800
4826
  if (recallLeave && botId) {
4801
4827
  try {
4802
- // Stop the transcript poller FIRST so no more transcript chunks get
4803
- // forwarded to the LLM during the leave.
4828
+ // Stop the transcript poller + live flush FIRST so no more chunks
4829
+ // get forwarded to the LLM during the leave.
4830
+ stopMeetingFlush();
4804
4831
  if (activeMeetingPoller) {
4805
4832
  activeMeetingPoller.stop();
4806
4833
  activeMeetingPoller = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.90",
3
+ "version": "0.9.91",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {