osborn 0.9.153 → 0.9.155

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.
@@ -45,7 +45,8 @@ RUN apt-get update -qq && \
45
45
  git \
46
46
  make \
47
47
  g++ \
48
- python-is-python3 && \
48
+ python-is-python3 \
49
+ util-linux && \
49
50
  rm -rf /var/lib/apt/lists/*
50
51
 
51
52
  ARG OSBORN_VERSION=latest
@@ -100,6 +101,7 @@ ENV OSBORN_CWD=/workspace
100
101
  # Caveat: native user modules are tied to the image's Node ABI (currently 22) —
101
102
  # a future Node-major image bump would need an `npm rebuild` of volume globals.
102
103
  ENV NPM_CONFIG_PREFIX=/workspace/.npm-global
104
+ ENV NPM_CONFIG_CACHE=/tmp/npm-cache
103
105
  ENV PATH=/workspace/.npm-global/bin:$PATH
104
106
 
105
107
  WORKDIR /workspace
@@ -122,6 +124,25 @@ fi
122
124
  echo "[sandbox-d] === boot at $(date -Iseconds) ===" >> "$LOGFILE"
123
125
  exec > >(tee -a "$LOGFILE") 2>&1
124
126
 
127
+ # === Swap on volume ===
128
+ # 8GB swapfile on /workspace (persistent volume). Created once on first boot,
129
+ # reactivated on every subsequent boot (swapon state is lost across cold starts).
130
+ # Gives 8GB RAM + 8GB swap = 16GB addressable — pressure relief when the dev
131
+ # server or concurrent sub-agents spike memory without killing the voice loop.
132
+ SWAPFILE=/workspace/swapfile
133
+ if [ ! -f "$SWAPFILE" ]; then
134
+ echo "[sandbox-d] Creating 8GB swapfile at $SWAPFILE..."
135
+ fallocate -l 8G "$SWAPFILE" 2>/dev/null || dd if=/dev/zero of="$SWAPFILE" bs=1M count=8192 status=none
136
+ chmod 600 "$SWAPFILE"
137
+ mkswap "$SWAPFILE"
138
+ echo "[sandbox-d] Swapfile created"
139
+ fi
140
+ if swapon "$SWAPFILE" 2>/dev/null; then
141
+ echo "[sandbox-d] Swap active: $(free -h | grep Swap)"
142
+ else
143
+ echo "[sandbox-d] swapon skipped (already active or kernel restriction)"
144
+ fi
145
+
125
146
  # Onboarding-suppression JSON
126
147
  ONBOARDING_JSON='{"numStartups":10,"installMethod":"npm","autoUpdates":false,"hasCompletedOnboarding":true,"hasTrustDialogAccepted":true,"hasTrustDialogHooksAccepted":true,"hasCompletedProjectOnboarding":true,"hasAcknowledgedCostThreshold":true,"effortCalloutV2Dismissed":true,"theme":"dark","projects":{"/workspace":{"hasTrustDialogAccepted":true,"hasTrustDialogHooksAccepted":true,"hasCompletedProjectOnboarding":true}}}'
127
148
 
package/dist/index.js CHANGED
@@ -1528,6 +1528,37 @@ function startApiServer(workingDir, port) {
1528
1528
  return;
1529
1529
  }
1530
1530
  // ────────────────────────────────────────────────────────────────────────
1531
+ // ── Port proxy: /proxy/PORT/* → localhost:PORT/proxy/PORT/* ─────────────
1532
+ // Forwards to a local dev server while PRESERVING the full path so that
1533
+ // basePath-aware apps (e.g. Next.js with basePath: '/proxy/3000') receive
1534
+ // the complete URL and can strip the prefix themselves. No cookie required —
1535
+ // the machine is per-user and Fly's HTTPS is the outer auth boundary.
1536
+ const portProxyMatch = url.pathname.match(/^\/proxy\/(\d+)(\/.*)?$/);
1537
+ if (portProxyMatch) {
1538
+ const targetPort = parseInt(portProxyMatch[1]);
1539
+ if (targetPort > 1024 && targetPort < 65536 && targetPort !== 8300 && targetPort !== parseInt(process.env.OSBORN_API_PORT || '8741', 10)) {
1540
+ const portProxyOptions = {
1541
+ hostname: '127.0.0.1',
1542
+ port: targetPort,
1543
+ path: url.pathname + (url.search || ''),
1544
+ method: req.method,
1545
+ headers: { ...req.headers, host: `127.0.0.1:${targetPort}` },
1546
+ };
1547
+ const portProxyReq = httpRequest(portProxyOptions, (portProxyRes) => {
1548
+ res.writeHead(portProxyRes.statusCode || 502, portProxyRes.headers);
1549
+ portProxyRes.pipe(res);
1550
+ });
1551
+ portProxyReq.on('error', () => {
1552
+ if (!res.headersSent) {
1553
+ res.writeHead(502);
1554
+ res.end('Port proxy error');
1555
+ }
1556
+ });
1557
+ req.pipe(portProxyReq);
1558
+ return;
1559
+ }
1560
+ }
1561
+ // ────────────────────────────────────────────────────────────────────────
1531
1562
  // ── IDE reverse proxy (cookie-gated fall-through) ────────────────────────
1532
1563
  // Forwards to code-server ONLY when:
1533
1564
  // 1. ideProxyEnabled is true (code-server is confirmed ready), AND
@@ -2709,7 +2740,7 @@ async function main() {
2709
2740
  const { readSessionHistory } = await import('./session-access.js');
2710
2741
  const history = readSessionHistory(sessionId, workingDir, {
2711
2742
  lastN: 10,
2712
- types: ['assistant'],
2743
+ types: ['assistant', 'tool_use'], // tool_use entries also carry Claude's spoken text
2713
2744
  });
2714
2745
  recentMessages = history
2715
2746
  .filter((m) => m.text)
@@ -2725,8 +2756,14 @@ async function main() {
2725
2756
  // BEFORE the previous TTS completed, and we may have already suppressed in-flight
2726
2757
  // tts_say events that arrived during that overlap).
2727
2758
  const carriedSuppressed = lastInterruption?.suppressedText ?? '';
2728
- lastInterruption = { spokenText: fullText, recentMessages, suppressedText: carriedSuppressed, timestamp: Date.now() };
2729
- console.log(`📋 Interruption context stored (text: ${fullText.length} chars, JSONL: ${recentMessages.length} chars, suppressed carried: ${carriedSuppressed.length} chars)`);
2759
+ lastInterruption = {
2760
+ spokenText: fullText,
2761
+ recentMessages,
2762
+ suppressedText: carriedSuppressed,
2763
+ fullBlockText: fullBlockText && fullBlockText !== fullText ? fullBlockText : undefined,
2764
+ timestamp: Date.now(),
2765
+ };
2766
+ console.log(`📋 Interruption context stored (text: ${fullText.length} chars, JSONL: ${recentMessages.length} chars, block: ${fullBlockText?.length ?? 0} chars, suppressed carried: ${carriedSuppressed.length} chars)`);
2730
2767
  }
2731
2768
  /**
2732
2769
  * Append text the agent tried to say while the user was speaking, but which we
@@ -2766,6 +2803,7 @@ async function main() {
2766
2803
  spokenText: lastInterruption.spokenText,
2767
2804
  recentMessages: lastInterruption.recentMessages,
2768
2805
  suppressedText: lastInterruption.suppressedText,
2806
+ fullBlockText: lastInterruption.fullBlockText,
2769
2807
  };
2770
2808
  lastInterruption = null;
2771
2809
  return ctx;
@@ -21,6 +21,8 @@ export interface InterruptionContext {
21
21
  * and can re-articulate the relevant bits in its next response.
22
22
  */
23
23
  suppressedText: string;
24
+ /** Full text of the interrupted TTS block — fallback when JSONL read returns empty */
25
+ fullBlockText?: string;
24
26
  }
25
27
  export interface PipelineDirectOptions extends ClaudeLLMOptions {
26
28
  onFastBrainResult?: (result: FastBrainPanelResult) => void;
@@ -204,7 +204,10 @@ export class PipelineDirectLLM extends llm.LLM {
204
204
  `Anything in "Your recent messages" below that appears AFTER the quoted heard text is content the user did not hear. The user has no memory of it.`,
205
205
  ``,
206
206
  `Your recent messages (full untruncated — you wrote these):`,
207
- interruptCtx.recentMessages || '(no recent messages found)',
207
+ interruptCtx.recentMessages
208
+ || (interruptCtx.fullBlockText
209
+ ? `(JSONL unavailable — full TTS block text follows)\n"${interruptCtx.fullBlockText}"`
210
+ : '(no recent messages found)'),
208
211
  suppressedBlock,
209
212
  ``,
210
213
  `User's message: "${userText}"`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.153",
3
+ "version": "0.9.155",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {