osborn 0.9.230 → 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/index.js +108 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2014,6 +2014,10 @@ async function main() {
|
|
|
2014
2014
|
let currentLLM = null;
|
|
2015
2015
|
const slots = new Map();
|
|
2016
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;
|
|
2017
2021
|
// Agent-side "alone in room" leave timer (see Room-presence lifecycle note up
|
|
2018
2022
|
// top). Armed in ParticipantDisconnected once a user has left; if no one
|
|
2019
2023
|
// rejoins within the grace window the agent leaves LiveKit on its own.
|
|
@@ -2028,7 +2032,31 @@ async function main() {
|
|
|
2028
2032
|
// ParticipantDisconnected (covers a finished session). Cancelled the moment a
|
|
2029
2033
|
// user joins. Net invariant: the agent never holds an empty room beyond
|
|
2030
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
|
+
};
|
|
2031
2057
|
const armAloneTimer = () => {
|
|
2058
|
+
if (agentsActive())
|
|
2059
|
+
return;
|
|
2032
2060
|
if (aloneTimer)
|
|
2033
2061
|
clearTimeout(aloneTimer);
|
|
2034
2062
|
aloneTimer = null;
|
|
@@ -2054,6 +2082,8 @@ async function main() {
|
|
|
2054
2082
|
let fastLeaveTimer = null;
|
|
2055
2083
|
const POST_LEAVE_GRACE_MS = 20 * 1000;
|
|
2056
2084
|
const armFastLeaveTimer = () => {
|
|
2085
|
+
if (agentsActive())
|
|
2086
|
+
return;
|
|
2057
2087
|
if (fastLeaveTimer)
|
|
2058
2088
|
clearTimeout(fastLeaveTimer);
|
|
2059
2089
|
fastLeaveTimer = null;
|
|
@@ -2099,7 +2129,7 @@ async function main() {
|
|
|
2099
2129
|
idleExitTimer = null;
|
|
2100
2130
|
const stillIdle = livekitState.status === 'idle' || livekitState.status === 'failed';
|
|
2101
2131
|
const empty = !activeRoom || activeRoom.remoteParticipants.size === 0;
|
|
2102
|
-
if (stillIdle && empty) {
|
|
2132
|
+
if (stillIdle && empty && !agentsActive()) {
|
|
2103
2133
|
console.log(`⏻ [IDLE-EXIT] still idle after grace — exiting 0 so Fly stops the machine (billing stops; next Resume boots it)`);
|
|
2104
2134
|
// Give the log tee a beat to flush, then stop the machine.
|
|
2105
2135
|
setTimeout(() => process.exit(0), 2000);
|
|
@@ -2229,6 +2259,25 @@ async function main() {
|
|
|
2229
2259
|
console.error(`❌ Background slot ${slotId.substring(0, 8)}: resume failed — requested ${requestedSessionId?.substring(0, 8)} got ${actualSessionId?.substring(0, 8)}`);
|
|
2230
2260
|
sendToFrontend({ type: 'background_session_error', slotId, error: 'Session resume failed' }).catch(() => { });
|
|
2231
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
|
+
});
|
|
2232
2281
|
// Wire up the LLM's internal EventEmitter so hook-emitted events also flow.
|
|
2233
2282
|
bgLLM.events.on('session_id', (data) => bgEmitter.emit('session_id', data));
|
|
2234
2283
|
bgLLM.events.on('tool_blocked', (data) => bgEmitter.emit('tool_blocked', data));
|
|
@@ -2251,6 +2300,12 @@ async function main() {
|
|
|
2251
2300
|
settingSources: ['project', 'user'],
|
|
2252
2301
|
env: bgEnv,
|
|
2253
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;
|
|
2254
2309
|
// Cold-start: deliver an init message so the subprocess actually spawns
|
|
2255
2310
|
// and begins consuming the session JSONL.
|
|
2256
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.';
|
|
@@ -2451,6 +2506,7 @@ async function main() {
|
|
|
2451
2506
|
currentLLM = null;
|
|
2452
2507
|
clearPipelineFastBrainSession();
|
|
2453
2508
|
clearPipelineFastBrainSession();
|
|
2509
|
+
releaseAgents(); // session over → clear host + kill slots so idle-exit can fire
|
|
2454
2510
|
armIdleExitTimer(`meeting ended (${reason}), no user`);
|
|
2455
2511
|
}
|
|
2456
2512
|
};
|
|
@@ -3254,6 +3310,15 @@ async function main() {
|
|
|
3254
3310
|
// PipelineDirectLLM (llmOverride path) wraps its own ClaudeLLM and may not expose setTurbo.
|
|
3255
3311
|
directLLM?.setTurbo?.(turboMode);
|
|
3256
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;
|
|
3257
3322
|
// Reset the session always-allow list for each new direct session
|
|
3258
3323
|
sessionAlwaysAllowPaths = new Set();
|
|
3259
3324
|
// For resumed sessions, eagerly create workspace (we know the real ID)
|
|
@@ -3884,6 +3949,7 @@ async function main() {
|
|
|
3884
3949
|
console.log('🕊️ Left LiveKit room intentionally — idle, awaiting /connect-room (no auto-rejoin)');
|
|
3885
3950
|
// 0.9.73: idle machines must not bill forever — stop the Fly machine
|
|
3886
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
|
|
3887
3953
|
armIdleExitTimer('intentional leave → idle');
|
|
3888
3954
|
return;
|
|
3889
3955
|
}
|
|
@@ -4843,6 +4909,12 @@ async function main() {
|
|
|
4843
4909
|
currentLLM = null;
|
|
4844
4910
|
clearPipelineFastBrainSession();
|
|
4845
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();
|
|
4846
4918
|
// Auto-leave path for a NON-meeting session. 0.9.83: a real session just
|
|
4847
4919
|
// ended → use the FAST leave (~20s), not the 3-min alone grace. Fires even
|
|
4848
4920
|
// on an abrupt tab close; cancelled if a user rejoins within the grace.
|
|
@@ -5747,6 +5819,39 @@ async function main() {
|
|
|
5747
5819
|
});
|
|
5748
5820
|
}
|
|
5749
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
|
+
}
|
|
5750
5855
|
else if (data.type === 'list_slots') {
|
|
5751
5856
|
// Return all live slots: the focused session + every background slot.
|
|
5752
5857
|
const focusedId = currentLLM?.sessionId || currentResumeSessionId || null;
|
|
@@ -5925,6 +6030,7 @@ async function main() {
|
|
|
5925
6030
|
if (!activeRoom) {
|
|
5926
6031
|
// Already idle — still ensure billing backstop is armed.
|
|
5927
6032
|
livekitState.status = 'idle';
|
|
6033
|
+
releaseAgents(); // ensure no lingering host/slots keep the machine alive
|
|
5928
6034
|
armIdleExitTimer(`destroyRoomSession(${reason}) — already idle`);
|
|
5929
6035
|
return;
|
|
5930
6036
|
}
|
|
@@ -5957,6 +6063,7 @@ async function main() {
|
|
|
5957
6063
|
livekitState.status = 'idle';
|
|
5958
6064
|
livekitState.error = null;
|
|
5959
6065
|
livekitState.errorCode = null;
|
|
6066
|
+
releaseAgents(); // session torn down → clear host + kill slots so idle-exit can fire
|
|
5960
6067
|
armIdleExitTimer(`destroyRoomSession(${reason})`);
|
|
5961
6068
|
destroyingRoomSession = false;
|
|
5962
6069
|
};
|