osborn 0.9.251 → 0.9.252-swap.0
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/dist/index.js +197 -77
- package/dist/pipeline-direct-llm.d.ts +4 -1
- package/dist/pipeline-direct-llm.js +30 -0
- package/package.json +1 -1
- package/dist/bridge-llm.d.ts +0 -22
- package/dist/bridge-llm.js +0 -39
- package/dist/codex-handler.d.ts +0 -38
- package/dist/codex-handler.js +0 -83
- package/dist/codex-llm.d.ts +0 -40
- package/dist/codex-llm.js +0 -144
- package/dist/fast-brain.d.ts +0 -176
- package/dist/fast-brain.js +0 -1887
- package/dist/index-3-2-26-legacy.d.ts +0 -1
- package/dist/index-3-2-26-legacy.js +0 -2233
- package/dist/leagcyprompts2.d.ts +0 -0
- package/dist/leagcyprompts2.js +0 -573
- package/dist/prompts-2-25-26.d.ts +0 -0
- package/dist/prompts-2-25-26.js +0 -518
- package/dist/prompts-3-2-26.d.ts +0 -78
- package/dist/prompts-3-2-26.js +0 -1319
- package/dist/summary-index.d.ts +0 -90
- package/dist/summary-index.js +0 -585
package/dist/index.js
CHANGED
|
@@ -2016,6 +2016,12 @@ async function main() {
|
|
|
2016
2016
|
let participantConnectedHandler = null;
|
|
2017
2017
|
let currentAgent = null; // For updateChatCtx() context injection
|
|
2018
2018
|
let currentLLM = null;
|
|
2019
|
+
// Voice bus for instance-swap handoff (assigned in createDirectSession). Declared
|
|
2020
|
+
// here so the swap_foreground handler (different scope) can re-pipe the voice to a
|
|
2021
|
+
// promoted background slot. See the "Voice bus" block in createDirectSession.
|
|
2022
|
+
let voiceBus = null;
|
|
2023
|
+
let unpipeVoice = null;
|
|
2024
|
+
let pipeVoiceFrom = null;
|
|
2019
2025
|
const slots = new Map();
|
|
2020
2026
|
let focusedSlotId = null; // reserved for future focused-slot wiring; not used yet
|
|
2021
2027
|
// P1 host-agent anchor: the dashboard-started session that owns the room's env + keeps it alive.
|
|
@@ -2212,6 +2218,44 @@ async function main() {
|
|
|
2212
2218
|
// Output events are forwarded to the frontend data channel only,
|
|
2213
2219
|
// tagged with the slot id so the frontend can distinguish them.
|
|
2214
2220
|
// ============================================================
|
|
2221
|
+
// Attach a background slot's UI forwarders to the emitter its consumer emits on, and
|
|
2222
|
+
// return a disposer. Forwards turn events to the frontend tagged with slotId but
|
|
2223
|
+
// DELIBERATELY never forwards tts_say — a background slot must stay silent. Shared by
|
|
2224
|
+
// spawnBackgroundSession (fresh slot) and the swap demote path (adopted old foreground)
|
|
2225
|
+
// so both slot kinds behave identically, and so promotion can cleanly stop background
|
|
2226
|
+
// forwarding (call the disposer) before re-piping that same emitter to the voice bus.
|
|
2227
|
+
function attachBackgroundForwarders(emitter, slotId) {
|
|
2228
|
+
const handlers = [];
|
|
2229
|
+
const on = (ev, fn) => { emitter.on(ev, fn); handlers.push([ev, fn]); };
|
|
2230
|
+
on('assistant_text', ({ text }) => { sendToFrontend({ type: 'claude_output', slotId, text }).catch(() => { }); });
|
|
2231
|
+
on('tool_use', ({ name, input, agentRole, agentId }) => { sendToFrontend({ type: 'tool_use', slotId, name, input, agentRole, agentId }).catch(() => { }); });
|
|
2232
|
+
on('tool_result', ({ name, response, agentRole, agentId }) => { sendToFrontend({ type: 'tool_result', slotId, name, response, agentRole, agentId }).catch(() => { }); });
|
|
2233
|
+
on('tool_blocked', ({ name, reason }) => { sendToFrontend({ type: 'tool_blocked', slotId, name, reason }).catch(() => { }); });
|
|
2234
|
+
on('session_id', ({ sessionId: sid }) => { sendToFrontend({ type: 'background_session_ready', slotId, sessionId: sid }).catch(() => { }); });
|
|
2235
|
+
on('session_resume_failed', ({ requestedSessionId, actualSessionId }) => {
|
|
2236
|
+
console.error(`❌ Background slot ${slotId.substring(0, 8)}: resume failed — requested ${requestedSessionId?.substring(0, 8)} got ${actualSessionId?.substring(0, 8)}`);
|
|
2237
|
+
sendToFrontend({ type: 'background_session_error', slotId, error: 'Session resume failed' }).catch(() => { });
|
|
2238
|
+
});
|
|
2239
|
+
on('consumer_error', ({ message }) => {
|
|
2240
|
+
console.error(`❌ Background slot ${slotId.substring(0, 8)}: consumer error — ${message}`);
|
|
2241
|
+
sendToFrontend({ type: 'background_session_error', slotId, error: String(message || 'Background session failed to start') }).catch(() => { });
|
|
2242
|
+
});
|
|
2243
|
+
on('assistant_result', ({ text }) => {
|
|
2244
|
+
const slot = slots.get(slotId);
|
|
2245
|
+
if (!slot)
|
|
2246
|
+
return;
|
|
2247
|
+
if (!slot.kickoffDone) {
|
|
2248
|
+
slot.kickoffDone = true;
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
if (!slot.dispatchInFlight)
|
|
2252
|
+
return;
|
|
2253
|
+
slot.dispatchInFlight = false;
|
|
2254
|
+
sendToFrontend({ type: 'background_update', sessionId: slotId, stage: 'done', summary: (text || '').trim().slice(0, 500) }).catch(() => { });
|
|
2255
|
+
});
|
|
2256
|
+
return () => { for (const [ev, fn] of handlers)
|
|
2257
|
+
emitter.off(ev, fn); };
|
|
2258
|
+
}
|
|
2215
2259
|
async function spawnBackgroundSession(sessionId, bgWorkingDir) {
|
|
2216
2260
|
// Hard cap: focused session + background slots must not exceed 3 total.
|
|
2217
2261
|
// slots Map holds ONLY background slots; focused session is separate.
|
|
@@ -2224,73 +2268,30 @@ async function main() {
|
|
|
2224
2268
|
const slotWorkingDir = bgWorkingDir || workingDir;
|
|
2225
2269
|
const slotId = sessionId; // use sessionId as the slot id for traceability
|
|
2226
2270
|
console.log(`🔲 Spawning background slot: sessionId=${slotId.substring(0, 8)} cwd=${slotWorkingDir}`);
|
|
2227
|
-
// Create
|
|
2271
|
+
// Create the headless LLM in the SAME SHAPE as the foreground inner so it can be
|
|
2272
|
+
// promoted to the voice session with a simple re-pipe (no reconfiguration):
|
|
2273
|
+
// skipTTSQueue: true → its consumer emits `tts_say` (speech), exactly like the
|
|
2274
|
+
// foreground, instead of queue.put. All events land on the instance's own
|
|
2275
|
+
// `.events` emitter (the unified bus), so promote = pipe the voice bus from
|
|
2276
|
+
// slot.llm.events; background = forward those same events to the UI minus tts_say.
|
|
2228
2277
|
// permissionMode bypassPermissions: no dialogs (headless — nobody to click Allow).
|
|
2229
|
-
// No AgentSession, no room involvement.
|
|
2230
2278
|
const bgLLM = createClaudeLLM({
|
|
2231
2279
|
workingDirectory: slotWorkingDir,
|
|
2232
2280
|
sessionBaseDir: slotWorkingDir,
|
|
2233
2281
|
resumeSessionId: sessionId,
|
|
2234
2282
|
voiceMode: 'direct', // picks up the research system prompt
|
|
2235
|
-
skipTTSQueue:
|
|
2283
|
+
skipTTSQueue: true, // emit tts_say like the foreground (promotable)
|
|
2236
2284
|
permissionMode: 'bypassPermissions',
|
|
2237
2285
|
});
|
|
2238
2286
|
// Register slot BEFORE cold-starting so list_slots is accurate from the
|
|
2239
2287
|
// first moment even if the subprocess takes a few seconds to init.
|
|
2240
2288
|
const slot = { id: slotId, llm: bgLLM, isFocused: false, workingDir: slotWorkingDir };
|
|
2241
2289
|
slots.set(slotId, slot);
|
|
2242
|
-
//
|
|
2243
|
-
//
|
|
2244
|
-
//
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
bgEmitter.on('assistant_text', ({ text }) => {
|
|
2248
|
-
sendToFrontend({ type: 'claude_output', slotId, text }).catch(() => { });
|
|
2249
|
-
});
|
|
2250
|
-
bgEmitter.on('tool_use', ({ name, input, agentRole }) => {
|
|
2251
|
-
sendToFrontend({ type: 'tool_use', slotId, name, input, agentRole }).catch(() => { });
|
|
2252
|
-
});
|
|
2253
|
-
bgEmitter.on('tool_result', ({ name, response, agentRole }) => {
|
|
2254
|
-
sendToFrontend({ type: 'tool_result', slotId, name, response, agentRole }).catch(() => { });
|
|
2255
|
-
});
|
|
2256
|
-
bgEmitter.on('tool_blocked', ({ name, reason }) => {
|
|
2257
|
-
sendToFrontend({ type: 'tool_blocked', slotId, name, reason }).catch(() => { });
|
|
2258
|
-
});
|
|
2259
|
-
bgEmitter.on('session_id', ({ sessionId: sid }) => {
|
|
2260
|
-
sendToFrontend({ type: 'background_session_ready', slotId, sessionId: sid }).catch(() => { });
|
|
2261
|
-
});
|
|
2262
|
-
bgEmitter.on('session_resume_failed', ({ requestedSessionId, actualSessionId }) => {
|
|
2263
|
-
console.error(`❌ Background slot ${slotId.substring(0, 8)}: resume failed — requested ${requestedSessionId?.substring(0, 8)} got ${actualSessionId?.substring(0, 8)}`);
|
|
2264
|
-
sendToFrontend({ type: 'background_session_error', slotId, error: 'Session resume failed' }).catch(() => { });
|
|
2265
|
-
});
|
|
2266
|
-
// Cold-start crash in the background consumer (claude-llm #startBackgroundConsumer
|
|
2267
|
-
// catch): surface it so the frontend's "Spawning…" clears instead of hanging forever.
|
|
2268
|
-
bgEmitter.on('consumer_error', ({ message }) => {
|
|
2269
|
-
console.error(`❌ Background slot ${slotId.substring(0, 8)}: consumer error — ${message}`);
|
|
2270
|
-
sendToFrontend({ type: 'background_session_error', slotId, error: String(message || 'Background session failed to start') }).catch(() => { });
|
|
2271
|
-
});
|
|
2272
|
-
// P1: a real dispatch finished — notify the frontend. The [BACKGROUND_INIT]
|
|
2273
|
-
// ack also fires assistant_result, so gate on dispatchInFlight to skip it.
|
|
2274
|
-
bgEmitter.on('assistant_result', ({ text }) => {
|
|
2275
|
-
const slot = slots.get(slotId);
|
|
2276
|
-
if (!slot)
|
|
2277
|
-
return;
|
|
2278
|
-
// The [BACKGROUND_INIT] kickoff also fires assistant_result and, on the cold-spawn
|
|
2279
|
-
// dispatch path, can arrive AFTER dispatchInFlight was set. Consume that first result
|
|
2280
|
-
// as the kickoff ack so it's never mis-reported as (and never clobbers) the dispatch's
|
|
2281
|
-
// real answer.
|
|
2282
|
-
if (!slot.kickoffDone) {
|
|
2283
|
-
slot.kickoffDone = true;
|
|
2284
|
-
return;
|
|
2285
|
-
}
|
|
2286
|
-
if (!slot.dispatchInFlight)
|
|
2287
|
-
return; // only notify for a real dispatched instruction
|
|
2288
|
-
slot.dispatchInFlight = false;
|
|
2289
|
-
sendToFrontend({ type: 'background_update', sessionId: slotId, stage: 'done', summary: (text || '').trim().slice(0, 500) }).catch(() => { });
|
|
2290
|
-
});
|
|
2291
|
-
// Wire up the LLM's internal EventEmitter so hook-emitted events also flow.
|
|
2292
|
-
bgLLM.events.on('session_id', (data) => bgEmitter.emit('session_id', data));
|
|
2293
|
-
bgLLM.events.on('tool_blocked', (data) => bgEmitter.emit('tool_blocked', data));
|
|
2290
|
+
// Background UI forwarders on the instance's own emitter (the unified bus). Silent
|
|
2291
|
+
// (no tts_say). Disposer stored so promotion can stop background forwarding before
|
|
2292
|
+
// the same emitter is piped to the voice bus.
|
|
2293
|
+
const unforward = attachBackgroundForwarders(bgLLM.events, slotId);
|
|
2294
|
+
slot.unforward = unforward;
|
|
2294
2295
|
// Build minimal sdkOptions for the headless cold start.
|
|
2295
2296
|
// resume: sessionId brings up real prior context from JSONL.
|
|
2296
2297
|
// env: CLAUDE_CODE_DISABLE_AUTO_MEMORY prevents concurrent subprocesses
|
|
@@ -2320,11 +2321,11 @@ async function main() {
|
|
|
2320
2321
|
settingSources: ['project', 'user'],
|
|
2321
2322
|
env: bgEnv,
|
|
2322
2323
|
};
|
|
2323
|
-
//
|
|
2324
|
-
// dispatch_background can reuse them.
|
|
2325
|
-
//
|
|
2326
|
-
//
|
|
2327
|
-
slot.emitter =
|
|
2324
|
+
// Persist the cold-start emitter + sdkOptions on the slot so a later
|
|
2325
|
+
// dispatch_background can reuse them. The slot's output emitter IS its own
|
|
2326
|
+
// bgLLM.events (unified bus) — pushMessage's fast path ignores the callbacks/emitter
|
|
2327
|
+
// arg once the subprocess exists, so the consumer stays bound to bgLLM.events.
|
|
2328
|
+
slot.emitter = bgLLM.events;
|
|
2328
2329
|
slot.sdkOptions = bgSdkOptions;
|
|
2329
2330
|
// Cold-start: deliver an init message so the subprocess actually spawns
|
|
2330
2331
|
// and begins consuming the session JSONL.
|
|
@@ -2332,10 +2333,10 @@ async function main() {
|
|
|
2332
2333
|
bgLLM.pushMessage(KICKOFF_TEXT, bgSdkOptions, {
|
|
2333
2334
|
onSessionId: (sid) => {
|
|
2334
2335
|
console.log(`✅ Background slot ${slotId.substring(0, 8)}: session confirmed ${sid.substring(0, 8)}`);
|
|
2335
|
-
|
|
2336
|
+
bgLLM.events.emit('session_id', { sessionId: sid });
|
|
2336
2337
|
},
|
|
2337
2338
|
onCheckpoint: (_ckpt) => { },
|
|
2338
|
-
eventEmitter:
|
|
2339
|
+
eventEmitter: bgLLM.events,
|
|
2339
2340
|
});
|
|
2340
2341
|
console.log(`✅ Background slot registered: id=${slotId.substring(0, 8)} workingDir=${slotWorkingDir}`);
|
|
2341
2342
|
await sendToFrontend({ type: 'background_session_spawned', slotId, workingDir: slotWorkingDir });
|
|
@@ -3352,6 +3353,31 @@ async function main() {
|
|
|
3352
3353
|
// PipelineDirectLLM (llmOverride path) wraps its own ClaudeLLM and may not expose setTurbo.
|
|
3353
3354
|
directLLM?.setTurbo?.(turboMode);
|
|
3354
3355
|
currentLLM = directLLM;
|
|
3356
|
+
// ── Voice bus (instance-swap handoff) ───────────────────────────────────────
|
|
3357
|
+
// The heavy LLM-event handlers below attach ONCE to this stable bus. A thin
|
|
3358
|
+
// pipe forwards events from whichever inner ClaudeLLM is currently foreground.
|
|
3359
|
+
// To hand the voice to a different, already-running subprocess (a background
|
|
3360
|
+
// slot) we just re-pipe the bus (swap_foreground) — the handlers never move,
|
|
3361
|
+
// and neither subprocess is killed. Handlers that need the live foreground LLM
|
|
3362
|
+
// (respondToPermission / suppressMeetingTTS) read `currentLLM`, which the swap
|
|
3363
|
+
// keeps pointed at the voice-owning wrapper.
|
|
3364
|
+
const { EventEmitter: VoiceBusEmitter } = await import('node:events');
|
|
3365
|
+
const vbus = new VoiceBusEmitter();
|
|
3366
|
+
vbus.setMaxListeners(50);
|
|
3367
|
+
voiceBus = vbus;
|
|
3368
|
+
const VOICE_EVENTS = ['mcp_servers_changed', 'tool_use', 'tool_result', 'assistant_text', 'assistant_result', 'tts_chunks', 'permission_request', 'tts_say', 'session_resume_failed', 'checkpoint_captured', 'dispatch_rejected', 'task_completed', 'agent_started'];
|
|
3369
|
+
pipeVoiceFrom = (emitter) => {
|
|
3370
|
+
const forwarders = [];
|
|
3371
|
+
for (const ev of VOICE_EVENTS) {
|
|
3372
|
+
const fn = (...args) => { vbus.emit(ev, ...args); };
|
|
3373
|
+
emitter.on(ev, fn);
|
|
3374
|
+
forwarders.push([ev, fn]);
|
|
3375
|
+
}
|
|
3376
|
+
return () => { for (const [ev, fn] of forwarders)
|
|
3377
|
+
emitter.off(ev, fn); };
|
|
3378
|
+
};
|
|
3379
|
+
// Pipe the initial foreground inner. Reassigned by swap_foreground.
|
|
3380
|
+
unpipeVoice = pipeVoiceFrom(directLLM.events);
|
|
3355
3381
|
// P1 host-agent anchor: this dashboard-started session owns the room's env
|
|
3356
3382
|
// and keeps it alive (agentsActive → true) so background dispatches survive.
|
|
3357
3383
|
// Keep-alive keys off the hostActive boolean, NOT a session_id event;
|
|
@@ -3383,7 +3409,7 @@ async function main() {
|
|
|
3383
3409
|
prewarmBM25Index(resumeSessionId, workingDir).catch(() => { });
|
|
3384
3410
|
}
|
|
3385
3411
|
// Wire up MCP server changes to frontend
|
|
3386
|
-
|
|
3412
|
+
vbus.on('mcp_servers_changed', (data) => {
|
|
3387
3413
|
console.log(`🔌 MCP servers changed: ${data.enabledKeys.join(', ') || 'none'}`);
|
|
3388
3414
|
sendToFrontend({
|
|
3389
3415
|
type: 'mcp_servers_changed',
|
|
@@ -3392,11 +3418,11 @@ async function main() {
|
|
|
3392
3418
|
});
|
|
3393
3419
|
});
|
|
3394
3420
|
// Wire up events from the Claude SDK wrapper to frontend
|
|
3395
|
-
|
|
3421
|
+
vbus.on('tool_use', (data) => {
|
|
3396
3422
|
console.log(`🔧 Claude: ${data.name}`);
|
|
3397
3423
|
sendToFrontend(buildToolLogEvent(data.name, data.input, 'running', data.agentRole || 'main', data.agentId));
|
|
3398
3424
|
});
|
|
3399
|
-
|
|
3425
|
+
vbus.on('tool_result', (data) => {
|
|
3400
3426
|
console.log(`✅ Done: ${data.name}`);
|
|
3401
3427
|
sendToFrontend(buildToolLogEvent(data.name, data.input, 'completed', data.agentRole || 'main', data.agentId));
|
|
3402
3428
|
// Detect research artifact writes (session workspace or legacy research dir)
|
|
@@ -3412,7 +3438,7 @@ async function main() {
|
|
|
3412
3438
|
}
|
|
3413
3439
|
});
|
|
3414
3440
|
// Wire up Claude text output - RAW text goes to frontend for chat bubbles
|
|
3415
|
-
|
|
3441
|
+
vbus.on('assistant_text', (data) => {
|
|
3416
3442
|
console.log(`💬 Claude text (${data.text?.length || 0} chars): ${data.text || ''}`);
|
|
3417
3443
|
sendToFrontend({
|
|
3418
3444
|
type: 'claude_output',
|
|
@@ -3424,7 +3450,7 @@ async function main() {
|
|
|
3424
3450
|
});
|
|
3425
3451
|
});
|
|
3426
3452
|
// Wire up Claude final result - RAW result goes to frontend
|
|
3427
|
-
|
|
3453
|
+
vbus.on('assistant_result', (data) => {
|
|
3428
3454
|
console.log(`📋 Claude result (${data.text?.length || 0} chars): ${data.text || ''}`);
|
|
3429
3455
|
sendToFrontend({
|
|
3430
3456
|
type: 'claude_output',
|
|
@@ -3436,7 +3462,7 @@ async function main() {
|
|
|
3436
3462
|
});
|
|
3437
3463
|
});
|
|
3438
3464
|
// Wire up ordered TTS chunk list — emitted once per turn for read-along
|
|
3439
|
-
|
|
3465
|
+
vbus.on('tts_chunks', (data) => {
|
|
3440
3466
|
sendToFrontend({
|
|
3441
3467
|
type: 'tts_chunks',
|
|
3442
3468
|
messageId: data.messageId,
|
|
@@ -3444,7 +3470,7 @@ async function main() {
|
|
|
3444
3470
|
});
|
|
3445
3471
|
});
|
|
3446
3472
|
// Wire up permission requests - sends to frontend for user approval
|
|
3447
|
-
|
|
3473
|
+
vbus.on('permission_request', (data) => {
|
|
3448
3474
|
console.log(`⚠️ Permission needed: ${data.toolName}`);
|
|
3449
3475
|
const toolName = data.toolName;
|
|
3450
3476
|
const input = data.input || {};
|
|
@@ -3453,7 +3479,7 @@ async function main() {
|
|
|
3453
3479
|
const filePath = String(input?.file_path || '');
|
|
3454
3480
|
if (filePath && sessionAlwaysAllowPaths.has(filePath)) {
|
|
3455
3481
|
console.log(`✅ Session always-allow: ${filePath}`);
|
|
3456
|
-
|
|
3482
|
+
currentLLM?.respondToPermission(true);
|
|
3457
3483
|
return;
|
|
3458
3484
|
}
|
|
3459
3485
|
}
|
|
@@ -3549,7 +3575,7 @@ async function main() {
|
|
|
3549
3575
|
});
|
|
3550
3576
|
// Wire up TTS say — bypass LiveKit's BufferedTokenStream, speak directly via session.say()
|
|
3551
3577
|
// Each text block from Claude gets spoken immediately as it arrives, no internal buffering
|
|
3552
|
-
|
|
3578
|
+
vbus.on('tts_say', (data) => {
|
|
3553
3579
|
if (!data.text?.trim()) {
|
|
3554
3580
|
console.log(`🔇 tts_say fired but text is empty — skipping`);
|
|
3555
3581
|
return;
|
|
@@ -3563,7 +3589,7 @@ async function main() {
|
|
|
3563
3589
|
// nulled currentSession and EVERY meeting reply was "text dropped" — the
|
|
3564
3590
|
// deaf-bot bug proven live 2026-08-01 via synthetic transcript inject.
|
|
3565
3591
|
// Set by PipelineDirectLLM.chat() when the turn is a [MEETING —] chunk.
|
|
3566
|
-
if (
|
|
3592
|
+
if (currentLLM.suppressMeetingTTS) {
|
|
3567
3593
|
// Parity relay (behind OSBORN_MEETING_AUDIO=parity) speaks via
|
|
3568
3594
|
// session.say, so it still needs a live session — it falls through to
|
|
3569
3595
|
// the normal browser-TTS path below only when currentSession exists.
|
|
@@ -3718,7 +3744,7 @@ async function main() {
|
|
|
3718
3744
|
}
|
|
3719
3745
|
});
|
|
3720
3746
|
// Wire up session resume failure - notify frontend when SDK creates new session instead
|
|
3721
|
-
|
|
3747
|
+
vbus.on('session_resume_failed', (data) => {
|
|
3722
3748
|
console.error(`❌ Session resume failed: ${data.requestedSessionId} → ${data.actualSessionId}`);
|
|
3723
3749
|
sendToFrontend({
|
|
3724
3750
|
type: 'session_resume_failed',
|
|
@@ -3727,7 +3753,7 @@ async function main() {
|
|
|
3727
3753
|
});
|
|
3728
3754
|
});
|
|
3729
3755
|
// Wire up file checkpoint capture - track restore points for file rewind
|
|
3730
|
-
|
|
3756
|
+
vbus.on('checkpoint_captured', (data) => {
|
|
3731
3757
|
console.log(`📍 Checkpoint: ${data.checkpointId.substring(0, 8)}...`);
|
|
3732
3758
|
sendToFrontend({
|
|
3733
3759
|
type: 'checkpoint_captured',
|
|
@@ -3735,7 +3761,7 @@ async function main() {
|
|
|
3735
3761
|
});
|
|
3736
3762
|
});
|
|
3737
3763
|
// Dispatcher v1 — writer reviewer rejected verdict → frontend
|
|
3738
|
-
|
|
3764
|
+
vbus.on('dispatch_rejected', (data) => {
|
|
3739
3765
|
console.log(`[DISPATCH] dispatch_rejected tuid=${(data.tuid ?? '').slice(0, 8)} sending to frontend`);
|
|
3740
3766
|
sendToFrontend({
|
|
3741
3767
|
type: 'task_review',
|
|
@@ -3745,14 +3771,14 @@ async function main() {
|
|
|
3745
3771
|
});
|
|
3746
3772
|
});
|
|
3747
3773
|
// Dispatcher v1 — sub-agent completed → frontend (Feature A)
|
|
3748
|
-
|
|
3774
|
+
vbus.on('task_completed', (d) => sendToFrontend({
|
|
3749
3775
|
type: 'task_completed',
|
|
3750
3776
|
agent_type: d.agent_type,
|
|
3751
3777
|
agent_id: d.agent_id,
|
|
3752
3778
|
last_assistant_message: d.last_assistant_message,
|
|
3753
3779
|
}));
|
|
3754
3780
|
// Dispatcher v1 — sub-agent started → frontend stop-key signal (Feature A2)
|
|
3755
|
-
|
|
3781
|
+
vbus.on('agent_started', (d) => sendToFrontend({
|
|
3756
3782
|
type: 'agent_started',
|
|
3757
3783
|
agent_type: d.agent_type,
|
|
3758
3784
|
agent_id: d.agent_id,
|
|
@@ -5937,6 +5963,10 @@ async function main() {
|
|
|
5937
5963
|
}
|
|
5938
5964
|
else {
|
|
5939
5965
|
const slot = slots.get(key);
|
|
5966
|
+
try {
|
|
5967
|
+
slot.unforward?.();
|
|
5968
|
+
}
|
|
5969
|
+
catch { } // remove its UI forwarders (no leak on repeat)
|
|
5940
5970
|
try {
|
|
5941
5971
|
slot.llm.abortQuery?.();
|
|
5942
5972
|
slot.llm.abortAgent?.();
|
|
@@ -5954,6 +5984,96 @@ async function main() {
|
|
|
5954
5984
|
armAloneTimer();
|
|
5955
5985
|
}
|
|
5956
5986
|
}
|
|
5987
|
+
else if (data.type === 'swap_foreground') {
|
|
5988
|
+
// Promote a background slot to the live voice session, and DEMOTE the current
|
|
5989
|
+
// foreground to a background slot — KEEPING BOTH SUBPROCESSES ALIVE (so the old
|
|
5990
|
+
// primary's in-flight sub-agents keep running). Mechanics:
|
|
5991
|
+
// 1. detach the voice bus from the old inner, 2. swapInner → the slot's ClaudeLLM,
|
|
5992
|
+
// 3. re-pipe the bus from the promoted inner's events, 4. adopt the old inner as a
|
|
5993
|
+
// background slot with its own UI-forwarding, 5. drop the promoted slot from the map.
|
|
5994
|
+
// The voice AgentSession is untouched (it holds the wrapper); no reconnect.
|
|
5995
|
+
const sid = data.sessionId;
|
|
5996
|
+
let key;
|
|
5997
|
+
if (sid) {
|
|
5998
|
+
if (slots.has(sid))
|
|
5999
|
+
key = sid;
|
|
6000
|
+
else
|
|
6001
|
+
key = [...slots.keys()].find(k => k === sid || k.startsWith(sid) || sid.startsWith(k));
|
|
6002
|
+
}
|
|
6003
|
+
const wrapper = currentLLM;
|
|
6004
|
+
if (!currentLLM || !voiceBus || !pipeVoiceFrom || typeof wrapper?.swapInner !== 'function') {
|
|
6005
|
+
await sendToFrontend({ type: 'background_session_error', slotId: sid ?? null, error: 'Swap unavailable (no live voice session or unsupported mode)' });
|
|
6006
|
+
}
|
|
6007
|
+
else if (!key) {
|
|
6008
|
+
await sendToFrontend({ type: 'background_session_error', slotId: sid ?? null, error: 'No matching background instance to promote' });
|
|
6009
|
+
}
|
|
6010
|
+
else {
|
|
6011
|
+
const slot = slots.get(key);
|
|
6012
|
+
const oldInner = wrapper.inner;
|
|
6013
|
+
// Require a CONFIRMED session id for the session being demoted — never fall back
|
|
6014
|
+
// to currentResumeSessionId, which can be stale from a prior swap and would tag the
|
|
6015
|
+
// demoted row (and its stop/re-promote key) with the wrong id. If the foreground
|
|
6016
|
+
// hasn't established its session yet (cold replay), refuse rather than mis-adopt.
|
|
6017
|
+
const oldId = oldInner?.sessionId || null;
|
|
6018
|
+
const oldWorkingDir = workingDir;
|
|
6019
|
+
if (!oldId || !oldInner) {
|
|
6020
|
+
await sendToFrontend({ type: 'background_session_error', slotId: sid ?? null, error: 'Foreground session not ready to demote yet — try again in a moment' });
|
|
6021
|
+
}
|
|
6022
|
+
else {
|
|
6023
|
+
console.log(`🔀 Swap foreground: promote ${key.substring(0, 8)} ⇄ demote ${oldId.substring(0, 8)}`);
|
|
6024
|
+
// PROMOTE — the slot was spawned in foreground shape (skipTTSQueue:true, events on
|
|
6025
|
+
// slot.llm.events). Stop forwarding it as a (silent) background row, then pipe the
|
|
6026
|
+
// voice bus from that SAME emitter so its tts_say/text/tools now drive the voice.
|
|
6027
|
+
try {
|
|
6028
|
+
slot.unforward?.();
|
|
6029
|
+
}
|
|
6030
|
+
catch { }
|
|
6031
|
+
try {
|
|
6032
|
+
unpipeVoice?.();
|
|
6033
|
+
}
|
|
6034
|
+
catch { }
|
|
6035
|
+
wrapper.swapInner(slot.llm); // also aborts old inner's in-flight fast-brain turn
|
|
6036
|
+
wrapper.setWorkingContext?.(slot.workingDir);
|
|
6037
|
+
unpipeVoice = pipeVoiceFrom(slot.llm.events);
|
|
6038
|
+
currentResumeSessionId = key;
|
|
6039
|
+
if (hostSessionId)
|
|
6040
|
+
hostSessionId = key;
|
|
6041
|
+
// DEMOTE — adopt the old foreground as a LIVE, full background slot (not killed).
|
|
6042
|
+
// Its consumer emits on oldInner.events, so attach the shared background forwarders
|
|
6043
|
+
// there (silent — no tts_say). Give it sdkOptions so dispatch_background accepts it,
|
|
6044
|
+
// and kickoffDone=true (it's already live; no [BACKGROUND_INIT] to consume).
|
|
6045
|
+
const demoteUnforward = attachBackgroundForwarders(oldInner.events, oldId);
|
|
6046
|
+
const demotedEnv = {};
|
|
6047
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
6048
|
+
if (v !== undefined)
|
|
6049
|
+
demotedEnv[k] = v;
|
|
6050
|
+
}
|
|
6051
|
+
demotedEnv['CLAUDE_CODE_DISABLE_AUTO_MEMORY'] = '1';
|
|
6052
|
+
demotedEnv['IS_SANDBOX'] = '1';
|
|
6053
|
+
const demoted = {
|
|
6054
|
+
id: oldId, llm: oldInner, isFocused: false, workingDir: oldWorkingDir,
|
|
6055
|
+
emitter: oldInner.events, unforward: demoteUnforward, kickoffDone: true,
|
|
6056
|
+
sdkOptions: { cwd: oldWorkingDir, resume: oldId, permissionMode: 'bypassPermissions', enableFileCheckpointing: false, settingSources: ['project', 'user'], env: demotedEnv },
|
|
6057
|
+
};
|
|
6058
|
+
slots.set(oldId, demoted);
|
|
6059
|
+
// The promoted session is now foreground — remove it from the slot registry.
|
|
6060
|
+
slots.delete(key);
|
|
6061
|
+
// Tell the frontend: reload the promoted session's context + refresh the panel.
|
|
6062
|
+
const summary = await getSessionSummary(key, workingDir).catch(() => null);
|
|
6063
|
+
const conversationHistory = await getConversationHistory(key, workingDir, 30).catch(() => []);
|
|
6064
|
+
await sendToFrontend({ type: 'session_switched', sessionId: key, success: true, summary, conversationHistory });
|
|
6065
|
+
const fId = key;
|
|
6066
|
+
const fEntry = [{ id: fId, isFocused: true, workingDir, hasSession: currentLLM.hasSession?.() ?? false }];
|
|
6067
|
+
const bEntries = [...slots.values()].map(s => ({ id: s.id, isFocused: s.isFocused, workingDir: s.workingDir, hasSession: s.llm.hasSession?.() ?? false }));
|
|
6068
|
+
await sendToFrontend({ type: 'slots_list', slots: [...fEntry, ...bEntries] });
|
|
6069
|
+
try {
|
|
6070
|
+
if (currentSession)
|
|
6071
|
+
await currentSession.say('Switched. This instance is live now — the previous one keeps running in the background.');
|
|
6072
|
+
}
|
|
6073
|
+
catch { }
|
|
6074
|
+
}
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
5957
6077
|
else if (data.type === 'list_slots') {
|
|
5958
6078
|
// Return all live slots: the focused session + every background slot.
|
|
5959
6079
|
const focusedId = currentLLM?.sessionId || currentResumeSessionId || null;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Phase 2 (future): Gemini speaks first, Claude suppressed when Gemini has HIGH confidence
|
|
10
10
|
*/
|
|
11
11
|
import { llm, type APIConnectOptions } from '@livekit/agents';
|
|
12
|
-
import { type ClaudeLLMOptions } from './claude-llm.js';
|
|
12
|
+
import { ClaudeLLM, type ClaudeLLMOptions } from './claude-llm.js';
|
|
13
13
|
import { EventEmitter } from 'events';
|
|
14
14
|
export interface InterruptionContext {
|
|
15
15
|
spokenText: string;
|
|
@@ -51,6 +51,9 @@ export declare class PipelineDirectLLM extends llm.LLM {
|
|
|
51
51
|
get model(): string;
|
|
52
52
|
get isResumingSession(): boolean;
|
|
53
53
|
label(): string;
|
|
54
|
+
get inner(): ClaudeLLM;
|
|
55
|
+
swapInner(next: ClaudeLLM): ClaudeLLM;
|
|
56
|
+
setWorkingContext(workingDirectory: string, sessionBaseDir?: string): void;
|
|
54
57
|
setResumeSessionId(id: string | null): void;
|
|
55
58
|
setContinueSession(e: boolean): void;
|
|
56
59
|
resetForSessionSwitch(): void;
|
|
@@ -94,6 +94,36 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
94
94
|
get model() { return this.#claudeLLM.model; }
|
|
95
95
|
get isResumingSession() { return this.#claudeLLM.isResumingSession; }
|
|
96
96
|
label() { return 'pipeline-direct'; }
|
|
97
|
+
// ── Instance swap (foreground ⇄ background handoff) ──────────────────────────
|
|
98
|
+
// The voice AgentSession binds THIS wrapper for the whole session; chat() routes
|
|
99
|
+
// to #claudeLLM. To move the voice to a different, already-running Claude
|
|
100
|
+
// subprocess (a background slot) WITHOUT tearing down the AgentSession or killing
|
|
101
|
+
// either subprocess, we hot-swap the inner ClaudeLLM. The caller is responsible
|
|
102
|
+
// for moving the voice event listeners from the old inner's emitter to the new
|
|
103
|
+
// one (wireVoice/unwireVoice in index.ts) — `.events` changes target after swap,
|
|
104
|
+
// but already-attached listeners stay on the old emitter until re-wired.
|
|
105
|
+
get inner() { return this.#claudeLLM; }
|
|
106
|
+
swapInner(next) {
|
|
107
|
+
const prev = this.#claudeLLM;
|
|
108
|
+
// Cancel any in-flight fast-brain turn bound to the OLD inner so its agentControl
|
|
109
|
+
// closures (which capture `this`) can't fire interrupt/sendPrompt into the newly
|
|
110
|
+
// promoted session. Clear per-turn flags so the new inner starts clean.
|
|
111
|
+
try {
|
|
112
|
+
this.#turnAbort?.abort();
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
this.#turnAbort = null;
|
|
116
|
+
this.#postCompactionTailPending = false;
|
|
117
|
+
this.#claudeLLM = next;
|
|
118
|
+
console.log('🔁 [pipeline] swapped inner ClaudeLLM (voice handoff)');
|
|
119
|
+
return prev;
|
|
120
|
+
}
|
|
121
|
+
// After a swap, point the dir-derived work (fast brain, session-tail) at the promoted
|
|
122
|
+
// session's working dir. Without this, #opts stays bound to the pre-swap session and a
|
|
123
|
+
// slot spawned with a different cwd would search the wrong store.
|
|
124
|
+
setWorkingContext(workingDirectory, sessionBaseDir) {
|
|
125
|
+
this.#opts = { ...this.#opts, workingDirectory, sessionBaseDir: sessionBaseDir ?? workingDirectory };
|
|
126
|
+
}
|
|
97
127
|
// Proxy all methods
|
|
98
128
|
setResumeSessionId(id) { this.#claudeLLM.setResumeSessionId(id); }
|
|
99
129
|
setContinueSession(e) { this.#claudeLLM.setContinueSession(e); }
|
package/package.json
CHANGED
package/dist/bridge-llm.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bridge LLM Module - Creates LLM instances for pipelined voice sessions
|
|
3
|
-
*
|
|
4
|
-
* In pipelined mode, we use a separate LLM (Gemini or GPT-4o) as the
|
|
5
|
-
* "conversation manager" that handles voice I/O and routes to Claude Code.
|
|
6
|
-
*/
|
|
7
|
-
import * as google from '@livekit/agents-plugin-google';
|
|
8
|
-
import * as openai from '@livekit/agents-plugin-openai';
|
|
9
|
-
export interface BridgeLLMConfig {
|
|
10
|
-
provider: 'gemini-pro' | 'gemini-flash' | 'gpt-4o' | 'gpt-4o-mini';
|
|
11
|
-
model?: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Create Bridge LLM instance for pipelined voice sessions
|
|
15
|
-
*
|
|
16
|
-
* Options:
|
|
17
|
-
* - gemini-pro: Gemini 2.5 Pro (smart, good reasoning)
|
|
18
|
-
* - gemini-flash: Gemini 2.0 Flash (faster, cheaper)
|
|
19
|
-
* - gpt-4o: GPT-4o (alternative if OpenAI preferred)
|
|
20
|
-
* - gpt-4o-mini: GPT-4o Mini (faster, cheaper)
|
|
21
|
-
*/
|
|
22
|
-
export declare function createBridgeLLM(config: BridgeLLMConfig): google.LLM | openai.LLM;
|
package/dist/bridge-llm.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bridge LLM Module - Creates LLM instances for pipelined voice sessions
|
|
3
|
-
*
|
|
4
|
-
* In pipelined mode, we use a separate LLM (Gemini or GPT-4o) as the
|
|
5
|
-
* "conversation manager" that handles voice I/O and routes to Claude Code.
|
|
6
|
-
*/
|
|
7
|
-
import * as google from '@livekit/agents-plugin-google';
|
|
8
|
-
import * as openai from '@livekit/agents-plugin-openai';
|
|
9
|
-
/**
|
|
10
|
-
* Create Bridge LLM instance for pipelined voice sessions
|
|
11
|
-
*
|
|
12
|
-
* Options:
|
|
13
|
-
* - gemini-pro: Gemini 2.5 Pro (smart, good reasoning)
|
|
14
|
-
* - gemini-flash: Gemini 2.0 Flash (faster, cheaper)
|
|
15
|
-
* - gpt-4o: GPT-4o (alternative if OpenAI preferred)
|
|
16
|
-
* - gpt-4o-mini: GPT-4o Mini (faster, cheaper)
|
|
17
|
-
*/
|
|
18
|
-
export function createBridgeLLM(config) {
|
|
19
|
-
switch (config.provider) {
|
|
20
|
-
case 'gemini-pro':
|
|
21
|
-
return new google.LLM({
|
|
22
|
-
model: config.model || 'gemini-2.5-pro',
|
|
23
|
-
});
|
|
24
|
-
case 'gemini-flash':
|
|
25
|
-
return new google.LLM({
|
|
26
|
-
model: config.model || 'gemini-2.0-flash',
|
|
27
|
-
});
|
|
28
|
-
case 'gpt-4o':
|
|
29
|
-
return new openai.LLM({
|
|
30
|
-
model: config.model || 'gpt-4o',
|
|
31
|
-
});
|
|
32
|
-
case 'gpt-4o-mini':
|
|
33
|
-
return new openai.LLM({
|
|
34
|
-
model: config.model || 'gpt-4o-mini',
|
|
35
|
-
});
|
|
36
|
-
default:
|
|
37
|
-
throw new Error(`Unknown Bridge LLM provider: ${config.provider}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
package/dist/codex-handler.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
type SandboxMode = 'workspace-write' | 'danger-full-access';
|
|
3
|
-
type ApprovalPolicy = 'on-failure' | 'on-request' | 'never';
|
|
4
|
-
interface CodexHandlerOptions {
|
|
5
|
-
workingDirectory?: string;
|
|
6
|
-
skipGitRepoCheck?: boolean;
|
|
7
|
-
sandboxMode?: SandboxMode;
|
|
8
|
-
approvalPolicy?: ApprovalPolicy;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Codex Handler using the official OpenAI Codex SDK
|
|
12
|
-
*
|
|
13
|
-
* Features:
|
|
14
|
-
* - Thread persistence (reuses same thread for context)
|
|
15
|
-
* - Built-in coding tools (file operations, terminal commands)
|
|
16
|
-
* - Hooks for observability
|
|
17
|
-
*
|
|
18
|
-
* Requirements:
|
|
19
|
-
* - Codex CLI must be installed (npm install -g @openai/codex or use npx)
|
|
20
|
-
* - Must be authenticated via: codex login (ChatGPT) or OPENAI_API_KEY env var
|
|
21
|
-
*
|
|
22
|
-
* Sandbox/Approval modes are configured via:
|
|
23
|
-
* - ~/.codex/config.yaml for global settings
|
|
24
|
-
* - CLI flags: --sandbox, --approval-policy
|
|
25
|
-
*
|
|
26
|
-
* Note: The SDK spawns the CLI binary, so authentication must be done beforehand.
|
|
27
|
-
*/
|
|
28
|
-
export declare class CodexHandler extends EventEmitter {
|
|
29
|
-
private codex;
|
|
30
|
-
private thread;
|
|
31
|
-
private options;
|
|
32
|
-
constructor(options?: CodexHandlerOptions);
|
|
33
|
-
run(prompt: string): Promise<string>;
|
|
34
|
-
getThreadId(): string | null;
|
|
35
|
-
clearThread(): void;
|
|
36
|
-
}
|
|
37
|
-
export declare function askCodex(prompt: string, cwd?: string): Promise<string>;
|
|
38
|
-
export {};
|