osborn 0.9.229 → 0.9.231
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/claude-llm.js +4 -4
- package/dist/index.js +123 -50
- package/dist/pipeline-direct-llm.d.ts +0 -2
- package/dist/pipeline-direct-llm.js +27 -51
- package/dist/session-store.d.ts +3 -1
- package/dist/session-store.js +8 -2
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -358,7 +358,7 @@ export const NAMED_AGENTS = {
|
|
|
358
358
|
'finding patterns, reading multiple files, searching for examples.',
|
|
359
359
|
'Returns structured findings — does NOT make decisions or edit files.',
|
|
360
360
|
'Use this for ANY task that needs more than 2 tool calls to gather information.',
|
|
361
|
-
'GROUNDED: reads the session
|
|
361
|
+
'GROUNDED: reads the session recall store (osborn-recall) for prior findings before researching.',
|
|
362
362
|
].join(' '),
|
|
363
363
|
tools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch', 'Task'],
|
|
364
364
|
grounded: true, // applyGrounding() injects the osborn-recall command + ensures Bash
|
|
@@ -419,7 +419,7 @@ export const NAMED_AGENTS = {
|
|
|
419
419
|
'Think hard about complex problems. Consider multiple approaches. Identify risks and edge cases.',
|
|
420
420
|
'',
|
|
421
421
|
'## Session context',
|
|
422
|
-
'
|
|
422
|
+
'You are deliberately NOT grounded (no raw session recall) — that keeps your analysis fresh and unbiased. Work from the STRUCTURED SUMMARIES the orchestrator hands you in your brief; do not attempt to pull raw session history yourself. If you need more context, ask the orchestrator for a structured summary rather than raw dumps.',
|
|
423
423
|
'',
|
|
424
424
|
'## How to work',
|
|
425
425
|
'1. Read and understand the full context before forming an opinion.',
|
|
@@ -461,7 +461,7 @@ export const NAMED_AGENTS = {
|
|
|
461
461
|
'Handles ALL file operations: code, config, docs, scripts, data files.',
|
|
462
462
|
'VERIFY-FIRST workflow: checks assumptions before making changes, runs tests after.',
|
|
463
463
|
'If anything is unclear, asks the main agent for clarification before touching files.',
|
|
464
|
-
'GROUNDED: reads the session
|
|
464
|
+
'GROUNDED: reads the session recall store (osborn-recall) before editing to avoid contradicting prior decisions.',
|
|
465
465
|
].join(' '),
|
|
466
466
|
tools: ['Read', 'Write', 'Edit', 'MultiEdit', 'Bash', 'Glob', 'Grep', 'NotebookRead', 'NotebookEdit'],
|
|
467
467
|
grounded: true, // applyGrounding() injects the osborn-recall command
|
|
@@ -612,7 +612,7 @@ export const NAMED_AGENTS = {
|
|
|
612
612
|
'ordered sequence of atomic writer-safe steps. Returns a self-contained brief the writer',
|
|
613
613
|
'can execute without further clarification. Slow but thorough — only use for genuinely',
|
|
614
614
|
'complex multi-file changes or when the approach is uncertain.',
|
|
615
|
-
'GROUNDED: reads the session
|
|
615
|
+
'GROUNDED: reads the session recall store (osborn-recall) before planning to respect prior decisions.',
|
|
616
616
|
].join(' '),
|
|
617
617
|
tools: ['Read', 'Glob', 'Grep', 'WebSearch'],
|
|
618
618
|
grounded: true, // applyGrounding() injects the osborn-recall command + adds Bash
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,6 @@ import { loadConfig, getMcpServers, getEnabledMcpServerNames, getVoiceMode, getD
|
|
|
37
37
|
import { createSTT, createTTS } from './voice-io.js';
|
|
38
38
|
import { createClaudeLLM, NAMED_AGENTS, applyTurbo } from './claude-llm.js';
|
|
39
39
|
import { clearPipelineFastBrainSession, prewarmBM25Index } from './pipeline-fastbrain.js';
|
|
40
|
-
import { getIndexPath, buildSummaryIndex } from './summary-index.js';
|
|
41
40
|
import { ensureClaudeAuth, applyAuthFallback } from './claude-auth.js';
|
|
42
41
|
import { createSmitheryProxy, destroySmitheryProxy, parseSmitheryUrl, isSmitheryUrl, SmitheryAuthorizationError } from './smithery-proxy.js';
|
|
43
42
|
import { DIRECT_MODE_PROMPT } from './prompts.js';
|
|
@@ -1823,47 +1822,29 @@ function startApiServer(workingDir, port) {
|
|
|
1823
1822
|
* OpenAI handles full history (30 exchanges, 2000 char content).
|
|
1824
1823
|
*/
|
|
1825
1824
|
/**
|
|
1826
|
-
*
|
|
1825
|
+
* Note to the voice agent's ChatContext on resume that prior conversation
|
|
1826
|
+
* history is searchable via the recall store.
|
|
1827
1827
|
*
|
|
1828
|
-
*
|
|
1829
|
-
*
|
|
1830
|
-
*
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1833
|
-
* Only exception: first-ever resume of a session that was never indexed (e.g. a
|
|
1834
|
-
* session started before the index watcher was added). In that case we build once.
|
|
1828
|
+
* Native --resume already replays the session into the SDK, so this is not a
|
|
1829
|
+
* context-injection path — it just reminds the agent that the FULL prior history
|
|
1830
|
+
* (this + previous sessions) is queryable through the `osborn-recall` command,
|
|
1831
|
+
* which reads the per-session recall store (session.db). We no longer inject the
|
|
1832
|
+
* old flat search-index.txt path or its `lineNum|…` line format (retired).
|
|
1835
1833
|
*/
|
|
1836
|
-
function injectSessionIndexIntoChatCtx(agent, sessionId,
|
|
1834
|
+
function injectSessionIndexIntoChatCtx(agent, sessionId, _workingDir) {
|
|
1837
1835
|
if (!agent || !sessionId)
|
|
1838
1836
|
return;
|
|
1839
1837
|
try {
|
|
1840
|
-
let indexPath = getIndexPath(sessionId, workingDir);
|
|
1841
|
-
if (!indexPath) {
|
|
1842
|
-
// First-ever resume with no index — build once, then point at it
|
|
1843
|
-
console.log(`🗂️ No index for ${sessionId.slice(0, 8)} — building (one-time)...`);
|
|
1844
|
-
try {
|
|
1845
|
-
const state = buildSummaryIndex(sessionId, workingDir);
|
|
1846
|
-
indexPath = (state.indexPath && existsSync(state.indexPath) && statSync(state.indexPath).size > 0)
|
|
1847
|
-
? state.indexPath : null;
|
|
1848
|
-
}
|
|
1849
|
-
catch {
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
if (!indexPath)
|
|
1854
|
-
return;
|
|
1855
|
-
// Pass the path — don't read the file. Native --resume already loaded the session;
|
|
1856
|
-
// this is just a reference the agent can use to search history if asked.
|
|
1857
1838
|
const chatCtx = agent.chatCtx.copy();
|
|
1858
1839
|
chatCtx.addMessage({
|
|
1859
1840
|
role: 'user',
|
|
1860
|
-
content: `[Session resume]
|
|
1841
|
+
content: `[Session resume] Prior conversation history is searchable via the \`osborn-recall\` command (hybrid keyword + semantic recall over the session store). Use it if the user asks about earlier work.`,
|
|
1861
1842
|
});
|
|
1862
1843
|
agent.updateChatCtx(chatCtx);
|
|
1863
|
-
console.log(`🗂️ Session
|
|
1844
|
+
console.log(`🗂️ Session recall note injected for ${sessionId.slice(0, 8)}`);
|
|
1864
1845
|
}
|
|
1865
1846
|
catch (err) {
|
|
1866
|
-
console.log('⚠️ Failed to inject session
|
|
1847
|
+
console.log('⚠️ Failed to inject session recall note:', err);
|
|
1867
1848
|
}
|
|
1868
1849
|
}
|
|
1869
1850
|
// Main function
|
|
@@ -2033,6 +2014,10 @@ async function main() {
|
|
|
2033
2014
|
let currentLLM = null;
|
|
2034
2015
|
const slots = new Map();
|
|
2035
2016
|
let focusedSlotId = null; // reserved for future focused-slot wiring; not used yet
|
|
2017
|
+
// P1 host-agent anchor: the dashboard-started session that owns the room's env + keeps it alive.
|
|
2018
|
+
let hostActive = false;
|
|
2019
|
+
let hostSessionId = null;
|
|
2020
|
+
let hostWorkingDir = null;
|
|
2036
2021
|
// Agent-side "alone in room" leave timer (see Room-presence lifecycle note up
|
|
2037
2022
|
// top). Armed in ParticipantDisconnected once a user has left; if no one
|
|
2038
2023
|
// rejoins within the grace window the agent leaves LiveKit on its own.
|
|
@@ -2047,7 +2032,31 @@ async function main() {
|
|
|
2047
2032
|
// ParticipantDisconnected (covers a finished session). Cancelled the moment a
|
|
2048
2033
|
// user joins. Net invariant: the agent never holds an empty room beyond
|
|
2049
2034
|
// ALONE_GRACE_MS, in any scenario — the root cause of the 25h burn.
|
|
2035
|
+
// P1: room stays alive while the host anchor is active or any background slot
|
|
2036
|
+
// is running. Teardown timers below early-return on this so a dashboard-started
|
|
2037
|
+
// session + its background dispatches aren't killed by the empty-room grace.
|
|
2038
|
+
const agentsActive = () => hostActive || slots.size > 0;
|
|
2039
|
+
// Release the host anchor + hard-kill background slots at a genuine session-end
|
|
2040
|
+
// boundary so the room can idle-exit (agentsActive → false). Idempotent. MUST be
|
|
2041
|
+
// called before arming idle-exit on EVERY path where the session ends, or the
|
|
2042
|
+
// empty-room billing burn returns — the keep-alive guard is only as safe as its
|
|
2043
|
+
// release sites (endMeeting / intentional-leave / destroyRoomSession / disconnect).
|
|
2044
|
+
const releaseAgents = () => {
|
|
2045
|
+
hostActive = false;
|
|
2046
|
+
hostSessionId = null;
|
|
2047
|
+
hostWorkingDir = null;
|
|
2048
|
+
for (const [, s] of slots) {
|
|
2049
|
+
try {
|
|
2050
|
+
s.llm.abortQuery?.();
|
|
2051
|
+
s.llm.abortAgent?.();
|
|
2052
|
+
}
|
|
2053
|
+
catch { }
|
|
2054
|
+
}
|
|
2055
|
+
slots.clear();
|
|
2056
|
+
};
|
|
2050
2057
|
const armAloneTimer = () => {
|
|
2058
|
+
if (agentsActive())
|
|
2059
|
+
return;
|
|
2051
2060
|
if (aloneTimer)
|
|
2052
2061
|
clearTimeout(aloneTimer);
|
|
2053
2062
|
aloneTimer = null;
|
|
@@ -2073,6 +2082,8 @@ async function main() {
|
|
|
2073
2082
|
let fastLeaveTimer = null;
|
|
2074
2083
|
const POST_LEAVE_GRACE_MS = 20 * 1000;
|
|
2075
2084
|
const armFastLeaveTimer = () => {
|
|
2085
|
+
if (agentsActive())
|
|
2086
|
+
return;
|
|
2076
2087
|
if (fastLeaveTimer)
|
|
2077
2088
|
clearTimeout(fastLeaveTimer);
|
|
2078
2089
|
fastLeaveTimer = null;
|
|
@@ -2118,7 +2129,7 @@ async function main() {
|
|
|
2118
2129
|
idleExitTimer = null;
|
|
2119
2130
|
const stillIdle = livekitState.status === 'idle' || livekitState.status === 'failed';
|
|
2120
2131
|
const empty = !activeRoom || activeRoom.remoteParticipants.size === 0;
|
|
2121
|
-
if (stillIdle && empty) {
|
|
2132
|
+
if (stillIdle && empty && !agentsActive()) {
|
|
2122
2133
|
console.log(`⏻ [IDLE-EXIT] still idle after grace — exiting 0 so Fly stops the machine (billing stops; next Resume boots it)`);
|
|
2123
2134
|
// Give the log tee a beat to flush, then stop the machine.
|
|
2124
2135
|
setTimeout(() => process.exit(0), 2000);
|
|
@@ -2172,21 +2183,10 @@ async function main() {
|
|
|
2172
2183
|
return;
|
|
2173
2184
|
try {
|
|
2174
2185
|
const llm = currentLLM;
|
|
2175
|
-
//
|
|
2176
|
-
//
|
|
2177
|
-
//
|
|
2178
|
-
//
|
|
2179
|
-
// three cleanup sites (Disconnected, previous-session-cleanup,
|
|
2180
|
-
// ParticipantDisconnected) call, but it previously only aborted the
|
|
2181
|
-
// SDK subprocess — leaving the interval (and the whole graph) alive and
|
|
2182
|
-
// uncollectable on every disconnect/reconnect. A reconnect-heavy session
|
|
2183
|
-
// (e.g. 15 reconnects from a frontend redeploy) leaked 15 timers + 15
|
|
2184
|
-
// retained graphs, each re-reading JSONL every 10s, until the node heap
|
|
2185
|
-
// OOM'd (~980MB) and the process crashed. Stopping the watcher here lets
|
|
2186
|
-
// the abandoned graph be GC'd. Duck-typed: only PipelineDirectLLM has it.
|
|
2187
|
-
if (typeof llm.stopIndexWatcher === 'function') {
|
|
2188
|
-
llm.stopIndexWatcher();
|
|
2189
|
-
}
|
|
2186
|
+
// NOTE (fast-brain migration): the old summary-index 10s setInterval watcher
|
|
2187
|
+
// was retired — fast-brain now reads the recall store (session.db) directly,
|
|
2188
|
+
// so there is no per-LLM timer holding the PipelineDirectLLM → ClaudeLLM graph
|
|
2189
|
+
// alive anymore. We only abort the SDK subprocess here.
|
|
2190
2190
|
if (typeof llm.abortQuery === 'function') {
|
|
2191
2191
|
llm.abortQuery();
|
|
2192
2192
|
}
|
|
@@ -2259,6 +2259,25 @@ async function main() {
|
|
|
2259
2259
|
console.error(`❌ Background slot ${slotId.substring(0, 8)}: resume failed — requested ${requestedSessionId?.substring(0, 8)} got ${actualSessionId?.substring(0, 8)}`);
|
|
2260
2260
|
sendToFrontend({ type: 'background_session_error', slotId, error: 'Session resume failed' }).catch(() => { });
|
|
2261
2261
|
});
|
|
2262
|
+
// P1: a real dispatch finished — notify the frontend. The [BACKGROUND_INIT]
|
|
2263
|
+
// ack also fires assistant_result, so gate on dispatchInFlight to skip it.
|
|
2264
|
+
bgEmitter.on('assistant_result', ({ text }) => {
|
|
2265
|
+
const slot = slots.get(slotId);
|
|
2266
|
+
if (!slot)
|
|
2267
|
+
return;
|
|
2268
|
+
// The [BACKGROUND_INIT] kickoff also fires assistant_result and, on the cold-spawn
|
|
2269
|
+
// dispatch path, can arrive AFTER dispatchInFlight was set. Consume that first result
|
|
2270
|
+
// as the kickoff ack so it's never mis-reported as (and never clobbers) the dispatch's
|
|
2271
|
+
// real answer.
|
|
2272
|
+
if (!slot.kickoffDone) {
|
|
2273
|
+
slot.kickoffDone = true;
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (!slot.dispatchInFlight)
|
|
2277
|
+
return; // only notify for a real dispatched instruction
|
|
2278
|
+
slot.dispatchInFlight = false;
|
|
2279
|
+
sendToFrontend({ type: 'background_update', sessionId: slotId, stage: 'done', summary: (text || '').trim().slice(0, 500) }).catch(() => { });
|
|
2280
|
+
});
|
|
2262
2281
|
// Wire up the LLM's internal EventEmitter so hook-emitted events also flow.
|
|
2263
2282
|
bgLLM.events.on('session_id', (data) => bgEmitter.emit('session_id', data));
|
|
2264
2283
|
bgLLM.events.on('tool_blocked', (data) => bgEmitter.emit('tool_blocked', data));
|
|
@@ -2281,6 +2300,12 @@ async function main() {
|
|
|
2281
2300
|
settingSources: ['project', 'user'],
|
|
2282
2301
|
env: bgEnv,
|
|
2283
2302
|
};
|
|
2303
|
+
// P1: persist the cold-start emitter + sdkOptions on the slot so a later
|
|
2304
|
+
// dispatch_background can reuse them. CRITICAL: pushMessage's fast path
|
|
2305
|
+
// ignores the callbacks/emitter arg once the subprocess exists, so a
|
|
2306
|
+
// dispatch MUST reuse THIS emitter — otherwise its output goes nowhere.
|
|
2307
|
+
slot.emitter = bgEmitter;
|
|
2308
|
+
slot.sdkOptions = bgSdkOptions;
|
|
2284
2309
|
// Cold-start: deliver an init message so the subprocess actually spawns
|
|
2285
2310
|
// and begins consuming the session JSONL.
|
|
2286
2311
|
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.';
|
|
@@ -2481,6 +2506,7 @@ async function main() {
|
|
|
2481
2506
|
currentLLM = null;
|
|
2482
2507
|
clearPipelineFastBrainSession();
|
|
2483
2508
|
clearPipelineFastBrainSession();
|
|
2509
|
+
releaseAgents(); // session over → clear host + kill slots so idle-exit can fire
|
|
2484
2510
|
armIdleExitTimer(`meeting ended (${reason}), no user`);
|
|
2485
2511
|
}
|
|
2486
2512
|
};
|
|
@@ -3284,6 +3310,15 @@ async function main() {
|
|
|
3284
3310
|
// PipelineDirectLLM (llmOverride path) wraps its own ClaudeLLM and may not expose setTurbo.
|
|
3285
3311
|
directLLM?.setTurbo?.(turboMode);
|
|
3286
3312
|
currentLLM = directLLM;
|
|
3313
|
+
// P1 host-agent anchor: this dashboard-started session owns the room's env
|
|
3314
|
+
// and keeps it alive (agentsActive → true) so background dispatches survive.
|
|
3315
|
+
// Keep-alive keys off the hostActive boolean, NOT a session_id event;
|
|
3316
|
+
// hostSessionId is best-effort (the focused id is otherwise resolved as
|
|
3317
|
+
// currentLLM?.sessionId || currentResumeSessionId).
|
|
3318
|
+
hostActive = true;
|
|
3319
|
+
hostWorkingDir = workingDir;
|
|
3320
|
+
if (resumeSessionId)
|
|
3321
|
+
hostSessionId = resumeSessionId;
|
|
3287
3322
|
// Reset the session always-allow list for each new direct session
|
|
3288
3323
|
sessionAlwaysAllowPaths = new Set();
|
|
3289
3324
|
// For resumed sessions, eagerly create workspace (we know the real ID)
|
|
@@ -3914,6 +3949,7 @@ async function main() {
|
|
|
3914
3949
|
console.log('🕊️ Left LiveKit room intentionally — idle, awaiting /connect-room (no auto-rejoin)');
|
|
3915
3950
|
// 0.9.73: idle machines must not bill forever — stop the Fly machine
|
|
3916
3951
|
// after the grace window unless a /connect-room revives us first.
|
|
3952
|
+
releaseAgents(); // session over → clear host + kill slots so idle-exit can fire
|
|
3917
3953
|
armIdleExitTimer('intentional leave → idle');
|
|
3918
3954
|
return;
|
|
3919
3955
|
}
|
|
@@ -4576,10 +4612,6 @@ async function main() {
|
|
|
4576
4612
|
try {
|
|
4577
4613
|
// Reuse existing session ID so Claude SDK resumes where it left off
|
|
4578
4614
|
const recoverySessionId = currentLLM?.sessionId || resumeSessionId;
|
|
4579
|
-
// Stop old index watcher if it exists
|
|
4580
|
-
if (currentLLM && 'stopIndexWatcher' in currentLLM) {
|
|
4581
|
-
currentLLM.stopIndexWatcher();
|
|
4582
|
-
}
|
|
4583
4615
|
let result;
|
|
4584
4616
|
{
|
|
4585
4617
|
// Pipeline mode: recreate PipelineDirectLLM wrapper with fast brain
|
|
@@ -4877,6 +4909,12 @@ async function main() {
|
|
|
4877
4909
|
currentLLM = null;
|
|
4878
4910
|
clearPipelineFastBrainSession();
|
|
4879
4911
|
clearPipelineFastBrainSession();
|
|
4912
|
+
// P1 (CRITICAL FOR COST): the human left a non-meeting session. Clear the
|
|
4913
|
+
// host anchor AND hard-kill every background slot so their subprocesses
|
|
4914
|
+
// stop burning tokens. agentsActive() keeps the room alive on the host +
|
|
4915
|
+
// slots, so this is the ONLY safe place to release them — leaving a slot
|
|
4916
|
+
// alive here would defeat the empty-room teardown timers entirely.
|
|
4917
|
+
releaseAgents();
|
|
4880
4918
|
// Auto-leave path for a NON-meeting session. 0.9.83: a real session just
|
|
4881
4919
|
// ended → use the FAST leave (~20s), not the 3-min alone grace. Fires even
|
|
4882
4920
|
// on an abrupt tab close; cancelled if a user rejoins within the grace.
|
|
@@ -5781,6 +5819,39 @@ async function main() {
|
|
|
5781
5819
|
});
|
|
5782
5820
|
}
|
|
5783
5821
|
}
|
|
5822
|
+
else if (data.type === 'dispatch_background') {
|
|
5823
|
+
// P1: push an instruction into a background slot (spawning it cold-start
|
|
5824
|
+
// first if needed), then notify the frontend when the turn completes.
|
|
5825
|
+
const targetSessionId = data.targetSessionId;
|
|
5826
|
+
const instruction = data.instruction;
|
|
5827
|
+
if (!targetSessionId || !instruction) {
|
|
5828
|
+
await sendToFrontend({ type: 'background_session_error', slotId: targetSessionId ?? null, error: 'dispatch_background requires targetSessionId + instruction' });
|
|
5829
|
+
}
|
|
5830
|
+
else {
|
|
5831
|
+
const focusedId = currentLLM?.sessionId || currentResumeSessionId || null;
|
|
5832
|
+
if (targetSessionId === focusedId) {
|
|
5833
|
+
await sendToFrontend({ type: 'background_session_error', slotId: targetSessionId, error: 'Target is the focused session — use the normal path' });
|
|
5834
|
+
}
|
|
5835
|
+
else {
|
|
5836
|
+
const doDispatch = () => {
|
|
5837
|
+
const slot = slots.get(targetSessionId);
|
|
5838
|
+
if (!slot || !slot.emitter || !slot.sdkOptions) {
|
|
5839
|
+
void sendToFrontend({ type: 'background_session_error', slotId: targetSessionId, error: 'Slot not ready' });
|
|
5840
|
+
return;
|
|
5841
|
+
}
|
|
5842
|
+
slot.dispatchInFlight = true;
|
|
5843
|
+
// Reuse the cold-start emitter — pushMessage's fast path ignores a
|
|
5844
|
+
// fresh emitter once the subprocess exists (see spawnBackgroundSession).
|
|
5845
|
+
slot.llm.pushMessage(instruction, slot.sdkOptions, { onSessionId: () => { }, onCheckpoint: () => { }, eventEmitter: slot.emitter });
|
|
5846
|
+
void sendToFrontend({ type: 'background_dispatched', sessionId: targetSessionId, instruction });
|
|
5847
|
+
};
|
|
5848
|
+
if (slots.has(targetSessionId))
|
|
5849
|
+
doDispatch();
|
|
5850
|
+
else
|
|
5851
|
+
spawnBackgroundSession(targetSessionId).then(doDispatch).catch((err) => { void sendToFrontend({ type: 'background_session_error', slotId: targetSessionId, error: err instanceof Error ? err.message : String(err) }); });
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5784
5855
|
else if (data.type === 'list_slots') {
|
|
5785
5856
|
// Return all live slots: the focused session + every background slot.
|
|
5786
5857
|
const focusedId = currentLLM?.sessionId || currentResumeSessionId || null;
|
|
@@ -5959,6 +6030,7 @@ async function main() {
|
|
|
5959
6030
|
if (!activeRoom) {
|
|
5960
6031
|
// Already idle — still ensure billing backstop is armed.
|
|
5961
6032
|
livekitState.status = 'idle';
|
|
6033
|
+
releaseAgents(); // ensure no lingering host/slots keep the machine alive
|
|
5962
6034
|
armIdleExitTimer(`destroyRoomSession(${reason}) — already idle`);
|
|
5963
6035
|
return;
|
|
5964
6036
|
}
|
|
@@ -5991,6 +6063,7 @@ async function main() {
|
|
|
5991
6063
|
livekitState.status = 'idle';
|
|
5992
6064
|
livekitState.error = null;
|
|
5993
6065
|
livekitState.errorCode = null;
|
|
6066
|
+
releaseAgents(); // session torn down → clear host + kill slots so idle-exit can fire
|
|
5994
6067
|
armIdleExitTimer(`destroyRoomSession(${reason})`);
|
|
5995
6068
|
destroyingRoomSession = false;
|
|
5996
6069
|
};
|
|
@@ -46,8 +46,6 @@ export declare class PipelineDirectLLM extends llm.LLM {
|
|
|
46
46
|
#private;
|
|
47
47
|
suppressMeetingTTS: boolean;
|
|
48
48
|
constructor(opts: PipelineDirectOptions);
|
|
49
|
-
/** Stop the index watcher (call on disconnect/session switch) */
|
|
50
|
-
stopIndexWatcher(): void;
|
|
51
49
|
get events(): EventEmitter;
|
|
52
50
|
get sessionId(): string | null;
|
|
53
51
|
get model(): string;
|
|
@@ -9,10 +9,9 @@
|
|
|
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';
|
|
13
12
|
import { ClaudeLLM } from './claude-llm.js';
|
|
14
13
|
import { askPipelineFastBrain } from './pipeline-fastbrain.js';
|
|
15
|
-
import {
|
|
14
|
+
import { storeExists, openStore, recentRows } from './session-store.js';
|
|
16
15
|
// ============================================================
|
|
17
16
|
// SESSION TAIL — compact recent-conversation block prepended to every user turn
|
|
18
17
|
// so the orchestrator never loses the thread across context compaction.
|
|
@@ -27,31 +26,33 @@ function buildSessionTail(sessionId, workingDir) {
|
|
|
27
26
|
return '';
|
|
28
27
|
const maxLines = parseInt(process.env.OSBORN_SESSION_TAIL_COUNT || '3000', 10);
|
|
29
28
|
try {
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
32
|
-
return '';
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
convLines.
|
|
29
|
+
const dbPath = storeExists(sessionId, workingDir);
|
|
30
|
+
if (!dbPath)
|
|
31
|
+
return ''; // no store → prior empty-index behavior
|
|
32
|
+
const db = openStore(dbPath, { readonly: true });
|
|
33
|
+
try {
|
|
34
|
+
// Pull the most recent user+assistant rows (filtered in SQL, so we only
|
|
35
|
+
// decompress rows we keep), newest-first, then reverse to chronological.
|
|
36
|
+
const rows = recentRows(db, maxLines, ['user', 'assistant']);
|
|
37
|
+
const convLines = [];
|
|
38
|
+
for (const row of rows) {
|
|
39
|
+
if (row.msgType !== 'user' && row.msgType !== 'assistant')
|
|
40
|
+
continue;
|
|
41
|
+
const timestamp = (row.ts || '').substring(0, 19);
|
|
42
|
+
const speaker = row.msgType === 'user' ? 'User' : 'Assistant';
|
|
43
|
+
const summary = row.text.replace(/[\n\r\t]+/g, ' ').replace(/\s+/g, ' ').trim().substring(0, 150);
|
|
44
|
+
convLines.push(`${timestamp} ${speaker}: ${summary}`);
|
|
45
|
+
}
|
|
46
|
+
// recentRows is newest-first — reverse to chronological, then keep the tail.
|
|
47
|
+
convLines.reverse();
|
|
48
|
+
const kept = convLines.slice(-maxLines);
|
|
49
|
+
if (kept.length === 0)
|
|
50
|
+
return '';
|
|
51
|
+
return ['<session_tail>', ...kept, '</session_tail>'].join('\n');
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
db.close();
|
|
50
55
|
}
|
|
51
|
-
const kept = convLines.slice(-maxLines);
|
|
52
|
-
if (kept.length === 0)
|
|
53
|
-
return '';
|
|
54
|
-
return ['<session_tail>', ...kept, '</session_tail>'].join('\n');
|
|
55
56
|
}
|
|
56
57
|
catch {
|
|
57
58
|
return '';
|
|
@@ -61,8 +62,6 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
61
62
|
#claudeLLM;
|
|
62
63
|
#opts;
|
|
63
64
|
#turnAbort = null;
|
|
64
|
-
#indexWatcher = null;
|
|
65
|
-
#indexBuilding = false;
|
|
66
65
|
// Set to true when PostCompact fires — cleared after injecting the tail once
|
|
67
66
|
// on the next real user turn (seam-only injection, never per-turn).
|
|
68
67
|
#postCompactionTailPending = false;
|
|
@@ -88,13 +87,6 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
88
87
|
this.#claudeLLM = new ClaudeLLM(chainedOpts);
|
|
89
88
|
this.#opts = chainedOpts;
|
|
90
89
|
}
|
|
91
|
-
/** Stop the index watcher (call on disconnect/session switch) */
|
|
92
|
-
stopIndexWatcher() {
|
|
93
|
-
if (this.#indexWatcher) {
|
|
94
|
-
this.#indexWatcher.stop();
|
|
95
|
-
this.#indexWatcher = null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
90
|
// Proxy all properties
|
|
99
91
|
get events() { return this.#claudeLLM.events; }
|
|
100
92
|
get sessionId() { return this.#claudeLLM.sessionId; }
|
|
@@ -105,8 +97,6 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
105
97
|
setResumeSessionId(id) { this.#claudeLLM.setResumeSessionId(id); }
|
|
106
98
|
setContinueSession(e) { this.#claudeLLM.setContinueSession(e); }
|
|
107
99
|
resetForSessionSwitch() {
|
|
108
|
-
this.stopIndexWatcher();
|
|
109
|
-
this.#indexBuilding = false;
|
|
110
100
|
this.#claudeLLM.resetForSessionSwitch();
|
|
111
101
|
}
|
|
112
102
|
respondToPermission(allow, msg) { this.#claudeLLM.respondToPermission(allow, msg); }
|
|
@@ -326,20 +316,6 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
326
316
|
}
|
|
327
317
|
const workingDir = this.#opts.workingDirectory || process.cwd();
|
|
328
318
|
const sessionBaseDir = this.#opts.sessionBaseDir || workingDir;
|
|
329
|
-
// Build summary index on first question (async, non-blocking for subsequent questions)
|
|
330
|
-
if (!this.#indexWatcher && !this.#indexBuilding && sessionId !== 'pending') {
|
|
331
|
-
this.#indexBuilding = true;
|
|
332
|
-
try {
|
|
333
|
-
const startBuild = Date.now();
|
|
334
|
-
const state = buildSummaryIndex(sessionId, workingDir, undefined, (msg) => console.log(`🔍 [index] ${msg}`));
|
|
335
|
-
this.#indexWatcher = startIndexWatcher(sessionId, workingDir, undefined, state);
|
|
336
|
-
console.log(`🔍 [index] Built + watching in ${Date.now() - startBuild}ms`);
|
|
337
|
-
}
|
|
338
|
-
catch (err) {
|
|
339
|
-
console.error('🔍 [index] Build failed:', err?.message);
|
|
340
|
-
}
|
|
341
|
-
this.#indexBuilding = false;
|
|
342
|
-
}
|
|
343
319
|
// NOTE: the embedded session.db write-through was CONSOLIDATED into the main agent's
|
|
344
320
|
// UserPromptSubmit hook (claude-llm.ts) — one canonical trigger per real user submission,
|
|
345
321
|
// main-thread only, sweeping main + all sub-agent JSONLs. Kept out of the per-turn pipeline
|
package/dist/session-store.d.ts
CHANGED
|
@@ -92,5 +92,7 @@ export declare function recall(db: Database.Database, query: string, opts?: Reca
|
|
|
92
92
|
* Return the most recent N content rows (highest id = latest, append-only order),
|
|
93
93
|
* newest first, as RecallHit-shaped objects. Powers fast-brain's "recent entries"
|
|
94
94
|
* path. Encapsulates decompress so callers never touch the blob/compression.
|
|
95
|
+
* `msgTypes` optionally filters by msg_type IN SQL — so callers that only want
|
|
96
|
+
* user/assistant rows don't decompress rows they'd discard.
|
|
95
97
|
*/
|
|
96
|
-
export declare function recentRows(db: Database.Database, count: number): RecallHit[];
|
|
98
|
+
export declare function recentRows(db: Database.Database, count: number, msgTypes?: string[]): RecallHit[];
|
package/dist/session-store.js
CHANGED
|
@@ -575,10 +575,16 @@ export async function recall(db, query, opts) {
|
|
|
575
575
|
* Return the most recent N content rows (highest id = latest, append-only order),
|
|
576
576
|
* newest first, as RecallHit-shaped objects. Powers fast-brain's "recent entries"
|
|
577
577
|
* path. Encapsulates decompress so callers never touch the blob/compression.
|
|
578
|
+
* `msgTypes` optionally filters by msg_type IN SQL — so callers that only want
|
|
579
|
+
* user/assistant rows don't decompress rows they'd discard.
|
|
578
580
|
*/
|
|
579
|
-
export function recentRows(db, count) {
|
|
581
|
+
export function recentRows(db, count, msgTypes) {
|
|
582
|
+
const filter = msgTypes && msgTypes.length
|
|
583
|
+
? `WHERE msg_type IN (${msgTypes.map(() => '?').join(', ')})`
|
|
584
|
+
: '';
|
|
585
|
+
const params = msgTypes && msgTypes.length ? [...msgTypes, Math.max(1, count)] : [Math.max(1, count)];
|
|
580
586
|
const rows = db.prepare(`SELECT id, source, line_num, byte_offset, ts, msg_type, model, git_branch, cwd, tool_name, blob
|
|
581
|
-
FROM content ORDER BY id DESC LIMIT ?`).all(
|
|
587
|
+
FROM content ${filter} ORDER BY id DESC LIMIT ?`).all(...params);
|
|
582
588
|
return rows.map((row) => ({
|
|
583
589
|
id: row.id,
|
|
584
590
|
source: row.source,
|