osborn 0.9.135 → 0.9.136
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/Dockerfile.sandbox +14 -0
- package/dist/pipeline-direct-llm.js +75 -3
- package/package.json +1 -1
package/Dockerfile.sandbox
CHANGED
|
@@ -205,6 +205,20 @@ if [ -d "$HOME/.claude/projects" ]; then
|
|
|
205
205
|
done
|
|
206
206
|
fi
|
|
207
207
|
|
|
208
|
+
|
|
209
|
+
# === Sync volume npm-global with image osborn version ===
|
|
210
|
+
# PATH prefers /workspace/.npm-global/bin over /usr/local/bin — so after an
|
|
211
|
+
# image-swap the volume copy is stale. Detect mismatch and sync on boot so
|
|
212
|
+
# the dashboard update button (image-swap + restart) actually takes effect.
|
|
213
|
+
VOL_OSBORN_VER=$(grep -m1 '"version"' /workspace/.npm-global/lib/node_modules/osborn/package.json 2>/dev/null | sed 's/.*"version"[^"]*"\([^"]*\)".*/\1/' || echo "none")
|
|
214
|
+
if [ -n "$IMAGE_SEED_VERSION" ] && [ "$IMAGE_SEED_VERSION" != "$VOL_OSBORN_VER" ]; then
|
|
215
|
+
echo "[sandbox-d] volume osborn@${VOL_OSBORN_VER} != image@${IMAGE_SEED_VERSION} -- syncing"
|
|
216
|
+
rm -rf /workspace/.npm-global/lib/node_modules/.osborn-* 2>/dev/null
|
|
217
|
+
npm install -g "osborn@${IMAGE_SEED_VERSION}" --no-audit --no-fund 2>&1 | tail -3
|
|
218
|
+
echo "[sandbox-d] sync done: osborn@${IMAGE_SEED_VERSION} on volume"
|
|
219
|
+
else
|
|
220
|
+
echo "[sandbox-d] volume osborn@${VOL_OSBORN_VER} matches image -- no sync needed"
|
|
221
|
+
fi
|
|
208
222
|
echo "[sandbox-d] exec'ing osborn (no chroot, HOME=$HOME)"
|
|
209
223
|
exec osborn
|
|
210
224
|
ENTRYPOINT
|
|
@@ -9,9 +9,54 @@
|
|
|
9
9
|
* Phase 2 (future): Gemini speaks first, Claude suppressed when Gemini has HIGH confidence
|
|
10
10
|
*/
|
|
11
11
|
import { llm, DEFAULT_API_CONNECT_OPTIONS } from '@livekit/agents';
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
12
13
|
import { ClaudeLLM } from './claude-llm.js';
|
|
13
14
|
import { askPipelineFastBrain } from './pipeline-fastbrain.js';
|
|
14
|
-
import { buildSummaryIndex, startIndexWatcher } from './summary-index.js';
|
|
15
|
+
import { buildSummaryIndex, startIndexWatcher, getIndexPath } from './summary-index.js';
|
|
16
|
+
// ============================================================
|
|
17
|
+
// SESSION TAIL — compact recent-conversation block prepended to every user turn
|
|
18
|
+
// so the orchestrator never loses the thread across context compaction.
|
|
19
|
+
// Controlled by:
|
|
20
|
+
// OSBORN_SESSION_TAIL — set to "0" to disable (default ON)
|
|
21
|
+
// OSBORN_SESSION_TAIL_COUNT — max user+assistant lines to keep (default 200)
|
|
22
|
+
// ============================================================
|
|
23
|
+
function buildSessionTail(sessionId, workingDir) {
|
|
24
|
+
if (process.env.OSBORN_SESSION_TAIL === '0')
|
|
25
|
+
return '';
|
|
26
|
+
if (!sessionId)
|
|
27
|
+
return '';
|
|
28
|
+
const maxLines = parseInt(process.env.OSBORN_SESSION_TAIL_COUNT || '200', 10);
|
|
29
|
+
try {
|
|
30
|
+
const indexPath = getIndexPath(sessionId, workingDir);
|
|
31
|
+
if (!indexPath)
|
|
32
|
+
return '';
|
|
33
|
+
const raw = readFileSync(indexPath, 'utf-8');
|
|
34
|
+
const allLines = raw.split('\n').filter(Boolean);
|
|
35
|
+
// Read a larger raw window (4× target) then keep the last maxLines user+assistant entries
|
|
36
|
+
const rawWindow = allLines.slice(-maxLines * 4);
|
|
37
|
+
const convLines = [];
|
|
38
|
+
for (const line of rawWindow) {
|
|
39
|
+
const parts = line.split('|');
|
|
40
|
+
// format: lineNum|byteOffset|timestamp|source|msgType|summary
|
|
41
|
+
if (parts.length < 6)
|
|
42
|
+
continue;
|
|
43
|
+
const msgType = parts[4];
|
|
44
|
+
if (msgType !== 'user' && msgType !== 'assistant')
|
|
45
|
+
continue;
|
|
46
|
+
const timestamp = parts[2];
|
|
47
|
+
const speaker = msgType === 'user' ? 'User' : 'Assistant';
|
|
48
|
+
const summary = parts.slice(5).join('|').substring(0, 150);
|
|
49
|
+
convLines.push(`${timestamp} ${speaker}: ${summary}`);
|
|
50
|
+
}
|
|
51
|
+
const kept = convLines.slice(-maxLines);
|
|
52
|
+
if (kept.length === 0)
|
|
53
|
+
return '';
|
|
54
|
+
return ['<session_tail>', ...kept, '</session_tail>'].join('\n');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
15
60
|
export class PipelineDirectLLM extends llm.LLM {
|
|
16
61
|
#claudeLLM;
|
|
17
62
|
#opts;
|
|
@@ -88,6 +133,17 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
88
133
|
}
|
|
89
134
|
}
|
|
90
135
|
console.log(`📥 [pipeline] chat() call #${callN} (${userText.length} chars): "${userText}"`);
|
|
136
|
+
// ── SESSION TAIL injection ────────────────────────────────────────────────
|
|
137
|
+
// Prepend a compact recent-conversation block so the orchestrator never
|
|
138
|
+
// loses thread across compaction. Runs synchronously (readFileSync) but the
|
|
139
|
+
// index file is ≤1MB and kept hot in the OS page cache — negligible latency.
|
|
140
|
+
// Skipped for meeting chunks (they are not real user turns).
|
|
141
|
+
const sessionTailBlock = (!userText.startsWith('[MEETING'))
|
|
142
|
+
? buildSessionTail(this.#claudeLLM.sessionId, this.#opts.workingDirectory || process.cwd())
|
|
143
|
+
: '';
|
|
144
|
+
if (sessionTailBlock) {
|
|
145
|
+
console.log(`🧵 [pipeline] Session tail: ${sessionTailBlock.split('\n').length - 2} lines ready`);
|
|
146
|
+
}
|
|
91
147
|
// Always check the pending playback context — it can carry two independent
|
|
92
148
|
// signals: (a) an actual interruption (spokenText + recentMessages) when the
|
|
93
149
|
// user cut Osborn off mid-TTS, OR (b) suppressed text generated by the SDK
|
|
@@ -171,11 +227,27 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
171
227
|
// when text is non-empty.
|
|
172
228
|
enrichedMessage = userText;
|
|
173
229
|
}
|
|
174
|
-
// Modify the last user message in chatCtx
|
|
230
|
+
// Modify the last user message in chatCtx — prepend session tail if available
|
|
231
|
+
const finalMessage = sessionTailBlock
|
|
232
|
+
? sessionTailBlock + '\n\n' + enrichedMessage
|
|
233
|
+
: enrichedMessage;
|
|
234
|
+
for (let i = chatCtx.items.length - 1; i >= 0; i--) {
|
|
235
|
+
const item = chatCtx.items[i];
|
|
236
|
+
if (item.type === 'message' && item.role === 'user') {
|
|
237
|
+
item.content = [finalMessage];
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (sessionTailBlock && userText.trim()) {
|
|
243
|
+
// No interrupt context — inject session tail directly into the user message
|
|
175
244
|
for (let i = chatCtx.items.length - 1; i >= 0; i--) {
|
|
176
245
|
const item = chatCtx.items[i];
|
|
177
246
|
if (item.type === 'message' && item.role === 'user') {
|
|
178
|
-
item.content
|
|
247
|
+
const original = Array.isArray(item.content)
|
|
248
|
+
? item.content.filter((c) => typeof c === 'string').join('\n')
|
|
249
|
+
: String(item.content ?? '');
|
|
250
|
+
item.content = [sessionTailBlock + '\n\n' + original];
|
|
179
251
|
break;
|
|
180
252
|
}
|
|
181
253
|
}
|