osborn 0.9.132 → 0.9.133
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 +161 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1689,6 +1689,8 @@ async function main() {
|
|
|
1689
1689
|
let participantConnectedHandler = null;
|
|
1690
1690
|
let currentAgent = null; // For updateChatCtx() context injection
|
|
1691
1691
|
let currentLLM = null;
|
|
1692
|
+
const slots = new Map();
|
|
1693
|
+
let focusedSlotId = null; // reserved for future focused-slot wiring; not used yet
|
|
1692
1694
|
// Agent-side "alone in room" leave timer (see Room-presence lifecycle note up
|
|
1693
1695
|
// top). Armed in ParticipantDisconnected once a user has left; if no one
|
|
1694
1696
|
// rejoins within the grace window the agent leaves LiveKit on its own.
|
|
@@ -1857,6 +1859,100 @@ async function main() {
|
|
|
1857
1859
|
console.error(`❌ killCurrentLLM(${reason}) failed:`, err instanceof Error ? err.message : err);
|
|
1858
1860
|
}
|
|
1859
1861
|
}
|
|
1862
|
+
// ============================================================
|
|
1863
|
+
// SLICE 1: spawnBackgroundSession
|
|
1864
|
+
// Creates a fully headless ClaudeLLM subprocess that resumes an
|
|
1865
|
+
// existing session. No AgentSession, no room, no TTS/voice.
|
|
1866
|
+
// Output events are forwarded to the frontend data channel only,
|
|
1867
|
+
// tagged with the slot id so the frontend can distinguish them.
|
|
1868
|
+
// ============================================================
|
|
1869
|
+
async function spawnBackgroundSession(sessionId, bgWorkingDir) {
|
|
1870
|
+
// Hard cap: focused session + background slots must not exceed 3 total.
|
|
1871
|
+
// slots Map holds ONLY background slots; focused session is separate.
|
|
1872
|
+
if (slots.size >= 2) {
|
|
1873
|
+
const msg = `Background session cap reached (${slots.size}/2 slots in use). Kill an existing background slot before spawning a new one.`;
|
|
1874
|
+
console.warn(`⚠️ spawnBackgroundSession: ${msg}`);
|
|
1875
|
+
await sendToFrontend({ type: 'background_session_error', slotId: sessionId, error: msg });
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
const slotWorkingDir = bgWorkingDir || workingDir;
|
|
1879
|
+
const slotId = sessionId; // use sessionId as the slot id for traceability
|
|
1880
|
+
console.log(`🔲 Spawning background slot: sessionId=${slotId.substring(0, 8)} cwd=${slotWorkingDir}`);
|
|
1881
|
+
// Create a headless LLM instance — reuse the same factory, no voice options.
|
|
1882
|
+
// permissionMode bypassPermissions: no dialogs (headless — nobody to click Allow).
|
|
1883
|
+
// No AgentSession, no room involvement.
|
|
1884
|
+
const bgLLM = createClaudeLLM({
|
|
1885
|
+
workingDirectory: slotWorkingDir,
|
|
1886
|
+
sessionBaseDir: slotWorkingDir,
|
|
1887
|
+
resumeSessionId: sessionId,
|
|
1888
|
+
voiceMode: 'direct', // picks up the research system prompt
|
|
1889
|
+
skipTTSQueue: false, // not going through TTS at all
|
|
1890
|
+
permissionMode: 'bypassPermissions',
|
|
1891
|
+
});
|
|
1892
|
+
// Register slot BEFORE cold-starting so list_slots is accurate from the
|
|
1893
|
+
// first moment even if the subprocess takes a few seconds to init.
|
|
1894
|
+
const slot = { id: slotId, llm: bgLLM, isFocused: false, workingDir: slotWorkingDir };
|
|
1895
|
+
slots.set(slotId, slot);
|
|
1896
|
+
// Dedicated EventEmitter for this background slot's SDK events.
|
|
1897
|
+
// Forwards every event to the frontend data channel tagged with slotId.
|
|
1898
|
+
// NEVER touches session.say() / TTS / voice.
|
|
1899
|
+
const { EventEmitter: SlotEventEmitter } = await import('node:events');
|
|
1900
|
+
const bgEmitter = new SlotEventEmitter();
|
|
1901
|
+
bgEmitter.on('assistant_text', ({ text }) => {
|
|
1902
|
+
sendToFrontend({ type: 'claude_output', slotId, text }).catch(() => { });
|
|
1903
|
+
});
|
|
1904
|
+
bgEmitter.on('tool_use', ({ name, input, agentRole }) => {
|
|
1905
|
+
sendToFrontend({ type: 'tool_use', slotId, name, input, agentRole }).catch(() => { });
|
|
1906
|
+
});
|
|
1907
|
+
bgEmitter.on('tool_result', ({ name, response, agentRole }) => {
|
|
1908
|
+
sendToFrontend({ type: 'tool_result', slotId, name, response, agentRole }).catch(() => { });
|
|
1909
|
+
});
|
|
1910
|
+
bgEmitter.on('tool_blocked', ({ name, reason }) => {
|
|
1911
|
+
sendToFrontend({ type: 'tool_blocked', slotId, name, reason }).catch(() => { });
|
|
1912
|
+
});
|
|
1913
|
+
bgEmitter.on('session_id', ({ sessionId: sid }) => {
|
|
1914
|
+
sendToFrontend({ type: 'background_session_ready', slotId, sessionId: sid }).catch(() => { });
|
|
1915
|
+
});
|
|
1916
|
+
bgEmitter.on('session_resume_failed', ({ requestedSessionId, actualSessionId }) => {
|
|
1917
|
+
console.error(`❌ Background slot ${slotId.substring(0, 8)}: resume failed — requested ${requestedSessionId?.substring(0, 8)} got ${actualSessionId?.substring(0, 8)}`);
|
|
1918
|
+
sendToFrontend({ type: 'background_session_error', slotId, error: 'Session resume failed' }).catch(() => { });
|
|
1919
|
+
});
|
|
1920
|
+
// Wire up the LLM's internal EventEmitter so hook-emitted events also flow.
|
|
1921
|
+
bgLLM.events.on('session_id', (data) => bgEmitter.emit('session_id', data));
|
|
1922
|
+
bgLLM.events.on('tool_blocked', (data) => bgEmitter.emit('tool_blocked', data));
|
|
1923
|
+
// Build minimal sdkOptions for the headless cold start.
|
|
1924
|
+
// resume: sessionId brings up real prior context from JSONL.
|
|
1925
|
+
// env: CLAUDE_CODE_DISABLE_AUTO_MEMORY prevents concurrent subprocesses
|
|
1926
|
+
// from racing the shared ~/.claude/CLAUDE.md memory file.
|
|
1927
|
+
// CLAUDE_CONFIG_DIR is intentionally NOT overridden — shared config is fine.
|
|
1928
|
+
const bgEnv = {};
|
|
1929
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1930
|
+
if (v !== undefined)
|
|
1931
|
+
bgEnv[k] = v;
|
|
1932
|
+
}
|
|
1933
|
+
bgEnv['CLAUDE_CODE_DISABLE_AUTO_MEMORY'] = '1';
|
|
1934
|
+
const bgSdkOptions = {
|
|
1935
|
+
cwd: slotWorkingDir,
|
|
1936
|
+
resume: sessionId,
|
|
1937
|
+
permissionMode: 'bypassPermissions',
|
|
1938
|
+
enableFileCheckpointing: false,
|
|
1939
|
+
settingSources: ['project', 'user'],
|
|
1940
|
+
env: bgEnv,
|
|
1941
|
+
};
|
|
1942
|
+
// Cold-start: deliver an init message so the subprocess actually spawns
|
|
1943
|
+
// and begins consuming the session JSONL.
|
|
1944
|
+
const KICKOFF_TEXT = '[BACKGROUND_INIT] You have been resumed in headless background mode. No voice session is active. Acknowledge receipt with a single brief line.';
|
|
1945
|
+
bgLLM.pushMessage(KICKOFF_TEXT, bgSdkOptions, {
|
|
1946
|
+
onSessionId: (sid) => {
|
|
1947
|
+
console.log(`✅ Background slot ${slotId.substring(0, 8)}: session confirmed ${sid.substring(0, 8)}`);
|
|
1948
|
+
bgEmitter.emit('session_id', { sessionId: sid });
|
|
1949
|
+
},
|
|
1950
|
+
onCheckpoint: (_ckpt) => { },
|
|
1951
|
+
eventEmitter: bgEmitter,
|
|
1952
|
+
});
|
|
1953
|
+
console.log(`✅ Background slot registered: id=${slotId.substring(0, 8)} workingDir=${slotWorkingDir}`);
|
|
1954
|
+
await sendToFrontend({ type: 'background_session_spawned', slotId, workingDir: slotWorkingDir });
|
|
1955
|
+
}
|
|
1860
1956
|
let localParticipant = null;
|
|
1861
1957
|
let agentState = 'initializing';
|
|
1862
1958
|
// Session-level always-allow list: paths the user has approved for this session without prompting
|
|
@@ -5607,6 +5703,71 @@ async function main() {
|
|
|
5607
5703
|
}
|
|
5608
5704
|
}
|
|
5609
5705
|
}
|
|
5706
|
+
// ============================================================
|
|
5707
|
+
// SLICE 1: HEADLESS BACKGROUND SESSION COMMANDS
|
|
5708
|
+
// spawn_background_session — boot a second Claude subprocess that
|
|
5709
|
+
// resumes an existing session with NO voice involvement.
|
|
5710
|
+
// list_slots — return all live slots (focused + background) so the
|
|
5711
|
+
// frontend can verify both are alive.
|
|
5712
|
+
// ============================================================
|
|
5713
|
+
else if (data.type === 'spawn_background_session') {
|
|
5714
|
+
const targetSessionId = data.sessionId;
|
|
5715
|
+
const bgDir = data.workingDir;
|
|
5716
|
+
let resolvedSessionId = targetSessionId;
|
|
5717
|
+
if (!resolvedSessionId) {
|
|
5718
|
+
// Pick the most recently modified session, excluding the focused one.
|
|
5719
|
+
const allSess = await listAllClaudeSessions(50);
|
|
5720
|
+
const focusedId = currentLLM?.sessionId || currentResumeSessionId || null;
|
|
5721
|
+
const candidate = allSess.find(s => s.sessionId !== focusedId);
|
|
5722
|
+
resolvedSessionId = candidate?.sessionId ?? undefined;
|
|
5723
|
+
}
|
|
5724
|
+
if (!resolvedSessionId) {
|
|
5725
|
+
await sendToFrontend({
|
|
5726
|
+
type: 'background_session_error',
|
|
5727
|
+
slotId: null,
|
|
5728
|
+
error: 'No eligible session found to spawn as background slot',
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
else if (slots.has(resolvedSessionId)) {
|
|
5732
|
+
await sendToFrontend({
|
|
5733
|
+
type: 'background_session_error',
|
|
5734
|
+
slotId: resolvedSessionId,
|
|
5735
|
+
error: `Session ${resolvedSessionId.substring(0, 8)} is already running as a background slot`,
|
|
5736
|
+
});
|
|
5737
|
+
}
|
|
5738
|
+
else {
|
|
5739
|
+
spawnBackgroundSession(resolvedSessionId, bgDir).catch((err) => {
|
|
5740
|
+
console.error('❌ spawnBackgroundSession failed:', err instanceof Error ? err.message : err);
|
|
5741
|
+
sendToFrontend({
|
|
5742
|
+
type: 'background_session_error',
|
|
5743
|
+
slotId: resolvedSessionId,
|
|
5744
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5745
|
+
}).catch(() => { });
|
|
5746
|
+
});
|
|
5747
|
+
}
|
|
5748
|
+
}
|
|
5749
|
+
else if (data.type === 'list_slots') {
|
|
5750
|
+
// Return all live slots: the focused session + every background slot.
|
|
5751
|
+
const focusedId = currentLLM?.sessionId || currentResumeSessionId || null;
|
|
5752
|
+
const focusedEntry = currentLLM
|
|
5753
|
+
? [{
|
|
5754
|
+
id: focusedId || '(pending)',
|
|
5755
|
+
isFocused: true,
|
|
5756
|
+
workingDir,
|
|
5757
|
+
hasSession: currentLLM.hasSession?.() ?? false,
|
|
5758
|
+
}]
|
|
5759
|
+
: [];
|
|
5760
|
+
const bgEntries = [...slots.values()].map(s => ({
|
|
5761
|
+
id: s.id,
|
|
5762
|
+
isFocused: s.isFocused,
|
|
5763
|
+
workingDir: s.workingDir,
|
|
5764
|
+
hasSession: s.llm.hasSession?.() ?? false,
|
|
5765
|
+
}));
|
|
5766
|
+
await sendToFrontend({
|
|
5767
|
+
type: 'slots_list',
|
|
5768
|
+
slots: [...focusedEntry, ...bgEntries],
|
|
5769
|
+
});
|
|
5770
|
+
}
|
|
5610
5771
|
}
|
|
5611
5772
|
catch { }
|
|
5612
5773
|
};
|