osborn 0.9.152 → 0.9.154
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 +21 -1
- package/dist/index.js +57 -8
- package/dist/pipeline-direct-llm.d.ts +2 -0
- package/dist/pipeline-direct-llm.js +4 -1
- package/package.json +1 -1
package/Dockerfile.sandbox
CHANGED
|
@@ -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
|
|
@@ -122,6 +123,25 @@ fi
|
|
|
122
123
|
echo "[sandbox-d] === boot at $(date -Iseconds) ===" >> "$LOGFILE"
|
|
123
124
|
exec > >(tee -a "$LOGFILE") 2>&1
|
|
124
125
|
|
|
126
|
+
# === Swap on volume ===
|
|
127
|
+
# 8GB swapfile on /workspace (persistent volume). Created once on first boot,
|
|
128
|
+
# reactivated on every subsequent boot (swapon state is lost across cold starts).
|
|
129
|
+
# Gives 8GB RAM + 8GB swap = 16GB addressable — pressure relief when the dev
|
|
130
|
+
# server or concurrent sub-agents spike memory without killing the voice loop.
|
|
131
|
+
SWAPFILE=/workspace/swapfile
|
|
132
|
+
if [ ! -f "$SWAPFILE" ]; then
|
|
133
|
+
echo "[sandbox-d] Creating 8GB swapfile at $SWAPFILE..."
|
|
134
|
+
fallocate -l 8G "$SWAPFILE" 2>/dev/null || dd if=/dev/zero of="$SWAPFILE" bs=1M count=8192 status=none
|
|
135
|
+
chmod 600 "$SWAPFILE"
|
|
136
|
+
mkswap "$SWAPFILE"
|
|
137
|
+
echo "[sandbox-d] Swapfile created"
|
|
138
|
+
fi
|
|
139
|
+
if swapon "$SWAPFILE" 2>/dev/null; then
|
|
140
|
+
echo "[sandbox-d] Swap active: $(free -h | grep Swap)"
|
|
141
|
+
else
|
|
142
|
+
echo "[sandbox-d] swapon skipped (already active or kernel restriction)"
|
|
143
|
+
fi
|
|
144
|
+
|
|
125
145
|
# Onboarding-suppression JSON
|
|
126
146
|
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
147
|
|
package/dist/index.js
CHANGED
|
@@ -298,6 +298,12 @@ wsProxy.on('error', (err, _req, socket) => {
|
|
|
298
298
|
// valid cookie are forwarded, so a forgotten agent route can never be silently
|
|
299
299
|
// swallowed by the IDE proxy. Cleared in stopIde().
|
|
300
300
|
let ideSessionToken = null;
|
|
301
|
+
// Cancellation-generation guard for startCodeServer(). Incremented by stopIde()
|
|
302
|
+
// every time the IDE is torn down. startCodeServer() captures myGen at entry;
|
|
303
|
+
// if ideStartGeneration !== myGen when the poll resolves, the start has been
|
|
304
|
+
// superseded (stopIde ran mid-flight) and the result is discarded instead of
|
|
305
|
+
// re-enabling the proxy with no participant present.
|
|
306
|
+
let ideStartGeneration = 0;
|
|
301
307
|
// Helper: parse the osborn_ide cookie from a request and return true iff it
|
|
302
308
|
// matches the current ideSessionToken (and a token exists).
|
|
303
309
|
function hasValidIdeCookie(req) {
|
|
@@ -1539,7 +1545,7 @@ function startApiServer(workingDir, port) {
|
|
|
1539
1545
|
port: 8300,
|
|
1540
1546
|
path: targetUrl.pathname + (targetUrl.search || ''),
|
|
1541
1547
|
method: req.method,
|
|
1542
|
-
headers: { ...req.headers, host: '127.0.0.1:8300' },
|
|
1548
|
+
headers: { ...req.headers, host: '127.0.0.1:8300', 'x-forwarded-host': req.headers.host },
|
|
1543
1549
|
};
|
|
1544
1550
|
const proxyReq = httpRequest(options, (proxyRes) => {
|
|
1545
1551
|
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
|
@@ -1602,6 +1608,13 @@ function startApiServer(workingDir, port) {
|
|
|
1602
1608
|
const isAgentRoute = AGENT_ROUTE_PREFIXES.some(p => upgradePath === p || upgradePath.startsWith(p + '/'));
|
|
1603
1609
|
if (ideProxyEnabled && hasValidIdeCookie(req) && !isAgentRoute) {
|
|
1604
1610
|
ideLastProxiedActivity = Date.now();
|
|
1611
|
+
// code-server's authenticateOrigin() compares Origin against the effective host
|
|
1612
|
+
// (X-Forwarded-Host first). changeOrigin:true rewrites Host→127.0.0.1:8300 while the
|
|
1613
|
+
// browser's Origin stays the public hostname, causing a 403 → WS 1006. Preserve the
|
|
1614
|
+
// real public host so the origin check matches.
|
|
1615
|
+
if (!req.headers['x-forwarded-host'] && req.headers.host) {
|
|
1616
|
+
req.headers['x-forwarded-host'] = req.headers.host;
|
|
1617
|
+
}
|
|
1605
1618
|
wsProxy.ws(req, socket, head, { target: IDE_TARGET });
|
|
1606
1619
|
return;
|
|
1607
1620
|
}
|
|
@@ -2339,6 +2352,9 @@ async function main() {
|
|
|
2339
2352
|
};
|
|
2340
2353
|
// Kill code-server, disable proxy, clear interval, reset IDE state.
|
|
2341
2354
|
const stopIde = (reason) => {
|
|
2355
|
+
// Bump the generation counter so any in-flight startCodeServer() knows it
|
|
2356
|
+
// has been cancelled and must not re-enable the proxy or mint a new token.
|
|
2357
|
+
ideStartGeneration++;
|
|
2342
2358
|
ideProxyEnabled = false;
|
|
2343
2359
|
ideSessionToken = null;
|
|
2344
2360
|
ideStartInProgress = null;
|
|
@@ -2412,6 +2428,11 @@ async function main() {
|
|
|
2412
2428
|
// Fresh start: kill any stale proc (shouldn't normally exist here, but be safe).
|
|
2413
2429
|
if (ideCodeServerProc)
|
|
2414
2430
|
stopIde();
|
|
2431
|
+
// Capture the current generation AFTER any preceding stopIde() call so that
|
|
2432
|
+
// if stopIde() already ran (above), myGen reflects the incremented counter.
|
|
2433
|
+
// A concurrent stopIde() after this point will increment ideStartGeneration
|
|
2434
|
+
// beyond myGen, signalling cancellation to the poll loop and success branch.
|
|
2435
|
+
const myGen = ideStartGeneration;
|
|
2415
2436
|
let resolveFn;
|
|
2416
2437
|
let rejectFn;
|
|
2417
2438
|
ideStartInProgress = new Promise((res, rej) => { resolveFn = res; rejectFn = rej; });
|
|
@@ -2462,11 +2483,13 @@ async function main() {
|
|
|
2462
2483
|
await new Promise((res, rej) => {
|
|
2463
2484
|
let attempts = 0;
|
|
2464
2485
|
const poll = setInterval(() => {
|
|
2465
|
-
// Early-exit:
|
|
2466
|
-
//
|
|
2467
|
-
|
|
2486
|
+
// Early-exit: stopIde() was called while we were waiting (generation bumped),
|
|
2487
|
+
// OR the proc we spawned is no longer the active one for another reason.
|
|
2488
|
+
// Either way, cancel the poll immediately so we don't keep hitting 8300
|
|
2489
|
+
// for a boot that has already been invalidated.
|
|
2490
|
+
if (ideStartGeneration !== myGen || ideCodeServerProc !== codeServerProc) {
|
|
2468
2491
|
clearInterval(poll);
|
|
2469
|
-
rej(new Error('code-server
|
|
2492
|
+
rej(new Error('code-server start cancelled (stopIde ran mid-poll)'));
|
|
2470
2493
|
return;
|
|
2471
2494
|
}
|
|
2472
2495
|
attempts++;
|
|
@@ -2487,6 +2510,25 @@ async function main() {
|
|
|
2487
2510
|
}, 500);
|
|
2488
2511
|
});
|
|
2489
2512
|
console.log('✅ code-server ready on 8300');
|
|
2513
|
+
// Generation guard: check again right before enabling the proxy. The poll
|
|
2514
|
+
// already checks this on each tick, but there is a narrow window between the
|
|
2515
|
+
// last tick resolving and reaching here. If stopIde() ran in that window,
|
|
2516
|
+
// ideStartGeneration will have been incremented beyond myGen — we must NOT
|
|
2517
|
+
// enable the proxy or mint a token, and we must kill the orphan process so
|
|
2518
|
+
// no zombie code-server is left running with no participant present.
|
|
2519
|
+
if (ideStartGeneration !== myGen) {
|
|
2520
|
+
console.log('🖥️ IDE start cancelled (stopIde ran while code-server was booting) — killing orphan proc');
|
|
2521
|
+
try {
|
|
2522
|
+
codeServerProc.kill();
|
|
2523
|
+
}
|
|
2524
|
+
catch { }
|
|
2525
|
+
// ideCodeServerProc may already be null (stopIde cleared it); only null it
|
|
2526
|
+
// if it still points to our proc to avoid clobbering a newer start.
|
|
2527
|
+
if (ideCodeServerProc === codeServerProc)
|
|
2528
|
+
ideCodeServerProc = null;
|
|
2529
|
+
resolveFn(); // resolve (not reject) so the promise chain exits cleanly
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2490
2532
|
// Mint the session cookie token and enable the proxy. From this point the
|
|
2491
2533
|
// /editor route will 302+Set-Cookie and the fall-through proxy will only
|
|
2492
2534
|
// forward requests that carry the valid osborn_ide cookie.
|
|
@@ -2667,7 +2709,7 @@ async function main() {
|
|
|
2667
2709
|
const { readSessionHistory } = await import('./session-access.js');
|
|
2668
2710
|
const history = readSessionHistory(sessionId, workingDir, {
|
|
2669
2711
|
lastN: 10,
|
|
2670
|
-
types: ['assistant'],
|
|
2712
|
+
types: ['assistant', 'tool_use'], // tool_use entries also carry Claude's spoken text
|
|
2671
2713
|
});
|
|
2672
2714
|
recentMessages = history
|
|
2673
2715
|
.filter((m) => m.text)
|
|
@@ -2683,8 +2725,14 @@ async function main() {
|
|
|
2683
2725
|
// BEFORE the previous TTS completed, and we may have already suppressed in-flight
|
|
2684
2726
|
// tts_say events that arrived during that overlap).
|
|
2685
2727
|
const carriedSuppressed = lastInterruption?.suppressedText ?? '';
|
|
2686
|
-
lastInterruption = {
|
|
2687
|
-
|
|
2728
|
+
lastInterruption = {
|
|
2729
|
+
spokenText: fullText,
|
|
2730
|
+
recentMessages,
|
|
2731
|
+
suppressedText: carriedSuppressed,
|
|
2732
|
+
fullBlockText: fullBlockText && fullBlockText !== fullText ? fullBlockText : undefined,
|
|
2733
|
+
timestamp: Date.now(),
|
|
2734
|
+
};
|
|
2735
|
+
console.log(`📋 Interruption context stored (text: ${fullText.length} chars, JSONL: ${recentMessages.length} chars, block: ${fullBlockText?.length ?? 0} chars, suppressed carried: ${carriedSuppressed.length} chars)`);
|
|
2688
2736
|
}
|
|
2689
2737
|
/**
|
|
2690
2738
|
* Append text the agent tried to say while the user was speaking, but which we
|
|
@@ -2724,6 +2772,7 @@ async function main() {
|
|
|
2724
2772
|
spokenText: lastInterruption.spokenText,
|
|
2725
2773
|
recentMessages: lastInterruption.recentMessages,
|
|
2726
2774
|
suppressedText: lastInterruption.suppressedText,
|
|
2775
|
+
fullBlockText: lastInterruption.fullBlockText,
|
|
2727
2776
|
};
|
|
2728
2777
|
lastInterruption = null;
|
|
2729
2778
|
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
|
|
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}"`,
|