osborn 0.9.135 → 0.9.137

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.
@@ -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 || '500', 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 = [enrichedMessage];
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.135",
3
+ "version": "0.9.137",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {