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.
- package/.claude/skills/meetings/SKILL.md +29 -0
- package/Dockerfile.sandbox +8 -1
- package/dist/index.js +49 -22
- package/package.json +1 -1
|
@@ -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:
|
package/Dockerfile.sandbox
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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
|
|
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;
|