osborn 0.8.7 → 0.8.8
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-auth.js +40 -0
- package/dist/claude-llm.js +7 -40
- package/dist/codex-llm.js +1 -1
- package/dist/index.js +10 -97
- package/dist/pipeline-direct-llm.js +1 -1
- package/dist/prompts.js +312 -25
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -9
- package/.claude/skills/markdown-to-pdf/SKILL.md +0 -29
- package/.claude/skills/pdf-to-markdown/SKILL.md +0 -28
- package/.claude/skills/playwright-browser/SKILL.md +0 -90
- package/.claude/skills/shadcn/SKILL.md +0 -232
- package/.claude/skills/shadcn/image.png +0 -0
- package/.claude/skills/youtube-transcript/SKILL.md +0 -24
- package/Dockerfile.sandbox +0 -59
- package/dist/conversation-brain.d.ts +0 -92
- package/dist/conversation-brain.js +0 -360
- package/dist/fast-llm.d.ts +0 -15
- package/dist/fast-llm.js +0 -81
package/dist/claude-auth.js
CHANGED
|
@@ -150,6 +150,13 @@ export async function checkClaudeAuthStatus() {
|
|
|
150
150
|
* Strips ALL whitespace first (like vutran1710/claudebox) to handle
|
|
151
151
|
* Ink UI wrapping the URL across multiple lines.
|
|
152
152
|
* Also cleans trailing "Pastecodehereifprompted" that Ink appends.
|
|
153
|
+
*
|
|
154
|
+
* IMPORTANT: strips the `redirect_uri` query parameter (which points to a
|
|
155
|
+
* localhost callback server on the *sprite*, not the user's machine). With
|
|
156
|
+
* no redirect_uri, claude.com falls back to showing the auth code in-page,
|
|
157
|
+
* which the user pastes back into the modal. This is the only flow that
|
|
158
|
+
* works for cloud sandboxes — the localhost redirect breaks both on phones
|
|
159
|
+
* (no listener) AND on desktops (sprite's localhost is unreachable).
|
|
153
160
|
*/
|
|
154
161
|
function extractOAuthUrl(text) {
|
|
155
162
|
// Strip ANSI codes
|
|
@@ -168,6 +175,39 @@ function extractOAuthUrl(text) {
|
|
|
168
175
|
if (idx > 0)
|
|
169
176
|
url = url.substring(0, idx);
|
|
170
177
|
}
|
|
178
|
+
// Strip the localhost redirect_uri so claude.com shows a pasteable code
|
|
179
|
+
// instead of trying to redirect. URL() can't be used here because it
|
|
180
|
+
// re-encodes the path, so we surgically delete the redirect_uri param.
|
|
181
|
+
url = stripRedirectUri(url);
|
|
182
|
+
return url;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Strip the `redirect_uri` query param from an OAuth URL.
|
|
186
|
+
*
|
|
187
|
+
* Background: `claude setup-token` spawns a one-shot localhost HTTP server on
|
|
188
|
+
* a random port and registers it as the redirect_uri. That works fine when the
|
|
189
|
+
* user is on the same machine as the CLI, but on a sprite the URL points to
|
|
190
|
+
* the *sprite's* localhost — unreachable from the user's browser regardless
|
|
191
|
+
* of whether they open the auth link on their PC or their phone. With no
|
|
192
|
+
* redirect_uri at all, claude.ai falls back to its in-page code display
|
|
193
|
+
* (the same flow that `claude setup-token`'s "Paste code here if prompted"
|
|
194
|
+
* Ink input is built to consume), and the user can paste the code back into
|
|
195
|
+
* our modal — which works whether they signed in on phone or desktop.
|
|
196
|
+
*
|
|
197
|
+
* Done with regex rather than `new URL()` because the URL constructor
|
|
198
|
+
* normalizes the path (which can break Claude's strict redirect check)
|
|
199
|
+
* and re-encodes spaces/special chars in other params.
|
|
200
|
+
*/
|
|
201
|
+
function stripRedirectUri(url) {
|
|
202
|
+
const before = url;
|
|
203
|
+
// Three cases: leading param (?redirect_uri=...&), middle/trailing (&redirect_uri=...),
|
|
204
|
+
// and only param (?redirect_uri=...). Order matters so cleanup leaves the URL well-formed.
|
|
205
|
+
url = url.replace(/&redirect_uri=[^&]*/g, '');
|
|
206
|
+
url = url.replace(/\?redirect_uri=[^&]*&/g, '?');
|
|
207
|
+
url = url.replace(/\?redirect_uri=[^&]*$/g, '');
|
|
208
|
+
if (before !== url) {
|
|
209
|
+
console.log('🔑 Stripped localhost redirect_uri from OAuth URL — claude.ai will show a pasteable code instead of redirecting');
|
|
210
|
+
}
|
|
171
211
|
return url;
|
|
172
212
|
}
|
|
173
213
|
// ─────────────────────────────────────────
|
package/dist/claude-llm.js
CHANGED
|
@@ -12,11 +12,7 @@ import { EventEmitter } from 'events';
|
|
|
12
12
|
import { saveSessionMetadata, getSessionWorkspace } from './config.js';
|
|
13
13
|
import { getResearchSystemPrompt, getDirectModeResearchPrompt } from './prompts.js';
|
|
14
14
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
15
|
-
import { join
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
// Directory of this module — used to locate co-located prompt files (e.g., turn-shape reminder).
|
|
18
|
-
const __claudeLlmDir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const TURN_SHAPE_REMINDER_PATH = join(__claudeLlmDir, 'prompts', 'turn-shape-reminder.md');
|
|
15
|
+
import { join } from 'node:path';
|
|
20
16
|
/**
|
|
21
17
|
* Strip markdown formatting for TTS (text-to-speech)
|
|
22
18
|
* Removes **bold**, ##headers, ```code```, etc. so TTS doesn't read them literally
|
|
@@ -621,7 +617,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
621
617
|
callbacks.eventEmitter.emit('assistant_text', { text: block.text });
|
|
622
618
|
const ttsChunk = stripMarkdownForTTS(block.text);
|
|
623
619
|
if (ttsChunk.trim()) {
|
|
624
|
-
console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
|
|
620
|
+
console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk.substring(0, 60)}..."`);
|
|
625
621
|
callbacks.eventEmitter.emit('tts_say', { text: ttsChunk });
|
|
626
622
|
}
|
|
627
623
|
}
|
|
@@ -736,7 +732,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
736
732
|
});
|
|
737
733
|
return;
|
|
738
734
|
}
|
|
739
|
-
console.log(`🎤 User (${userText.length
|
|
735
|
+
console.log(`🎤 User: "${userText.substring(0, 100)}${userText.length > 100 ? '...' : ''}"`);
|
|
740
736
|
// Build Claude Agent SDK options
|
|
741
737
|
const resumeSessionId = this.#opts.resumeSessionId;
|
|
742
738
|
const continueSession = this.#opts.continueSession;
|
|
@@ -856,35 +852,6 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
856
852
|
this.#eventEmitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse });
|
|
857
853
|
return {};
|
|
858
854
|
}]
|
|
859
|
-
}],
|
|
860
|
-
// Per-turn behavioral re-anchor. Fires on EVERY user message that reaches Claude
|
|
861
|
-
// (initial requests, follow-ups, mid-flight steering, resumed-session messages).
|
|
862
|
-
// Reads the reminder text from disk every call, so it's hot-editable just like the
|
|
863
|
-
// main prompt — edit agent/src/prompts/turn-shape-reminder.md, reconnect, next message
|
|
864
|
-
// sees the new reminder. The SDK injects `additionalContext` alongside the user's actual
|
|
865
|
-
// message so the model sees both the literal user input AND the reminder, weighing them
|
|
866
|
-
// together. This is what fights JSONL-history-overrides-system-prompt drift on resumed
|
|
867
|
-
// sessions: the conductor pattern gets re-asserted on every turn instead of being
|
|
868
|
-
// anchored only at session-init time.
|
|
869
|
-
UserPromptSubmit: [{
|
|
870
|
-
matcher: '.*',
|
|
871
|
-
hooks: [async (input) => {
|
|
872
|
-
try {
|
|
873
|
-
const reminder = readFileSync(TURN_SHAPE_REMINDER_PATH, 'utf-8');
|
|
874
|
-
const promptPreview = String(input?.prompt || '').substring(0, 60).replace(/\n/g, ' ');
|
|
875
|
-
console.log(`📌 UserPromptSubmit: injected turn-shape reminder (${reminder.length} chars) for prompt="${promptPreview}..."`);
|
|
876
|
-
return {
|
|
877
|
-
hookSpecificOutput: {
|
|
878
|
-
hookEventName: 'UserPromptSubmit',
|
|
879
|
-
additionalContext: reminder,
|
|
880
|
-
},
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
catch (err) {
|
|
884
|
-
console.error('⚠️ UserPromptSubmit: failed to load turn-shape-reminder.md:', err instanceof Error ? err.message : err);
|
|
885
|
-
return { hookSpecificOutput: { hookEventName: 'UserPromptSubmit' } };
|
|
886
|
-
}
|
|
887
|
-
}]
|
|
888
855
|
}]
|
|
889
856
|
},
|
|
890
857
|
// Named sub-agents — Haiku overseer delegates to these specialists.
|
|
@@ -1109,12 +1076,12 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1109
1076
|
if (this.#opts.skipTTSQueue) {
|
|
1110
1077
|
// Direct mode: emit event for session.say() — bypasses LiveKit's
|
|
1111
1078
|
// BufferedTokenStream which causes stuck/delayed/out-of-order audio
|
|
1112
|
-
console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
|
|
1079
|
+
console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk.substring(0, 60)}..."`);
|
|
1113
1080
|
this.#eventEmitter.emit('tts_say', { text: ttsChunk });
|
|
1114
1081
|
}
|
|
1115
1082
|
else {
|
|
1116
1083
|
// Realtime mode: use LLM stream queue (framework handles TTS)
|
|
1117
|
-
console.log(`🔊 TTS stream (${ttsChunk.length} chars): "${ttsChunk}"`);
|
|
1084
|
+
console.log(`🔊 TTS stream (${ttsChunk.length} chars): "${ttsChunk.substring(0, 60)}..."`);
|
|
1118
1085
|
this.queue.put({
|
|
1119
1086
|
id: requestId,
|
|
1120
1087
|
delta: { role: 'assistant', content: ttsChunk },
|
|
@@ -1134,11 +1101,11 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1134
1101
|
const ttsText = stripMarkdownForTTS(rawResult);
|
|
1135
1102
|
if (ttsText.trim()) {
|
|
1136
1103
|
if (this.#opts.skipTTSQueue) {
|
|
1137
|
-
console.log(`🔊 TTS say result (${ttsText.length} chars): "${ttsText}"`);
|
|
1104
|
+
console.log(`🔊 TTS say result (${ttsText.length} chars): "${ttsText.substring(0, 60)}..."`);
|
|
1138
1105
|
this.#eventEmitter.emit('tts_say', { text: ttsText });
|
|
1139
1106
|
}
|
|
1140
1107
|
else {
|
|
1141
|
-
console.log(`🔊 TTS result (${ttsText.length} chars): "${ttsText}"`);
|
|
1108
|
+
console.log(`🔊 TTS result (${ttsText.length} chars): "${ttsText.substring(0, 60)}..."`);
|
|
1142
1109
|
this.queue.put({
|
|
1143
1110
|
id: requestId,
|
|
1144
1111
|
delta: { role: 'assistant', content: ttsText },
|
package/dist/codex-llm.js
CHANGED
|
@@ -97,7 +97,7 @@ class CodexLLMStream extends llm.LLMStream {
|
|
|
97
97
|
});
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
|
-
console.log(`🎤 User (${userText.length
|
|
100
|
+
console.log(`🎤 User: "${userText.substring(0, 100)}${userText.length > 100 ? '...' : ''}"`);
|
|
101
101
|
// Create or reuse thread
|
|
102
102
|
if (!this.#thread) {
|
|
103
103
|
console.log('🆕 Starting new Codex thread');
|
package/dist/index.js
CHANGED
|
@@ -227,47 +227,6 @@ function startApiServer(workingDir, port) {
|
|
|
227
227
|
setTimeout(() => process.exit(0), 150);
|
|
228
228
|
return;
|
|
229
229
|
}
|
|
230
|
-
// GET /events — Server-Sent Events heartbeat for cloud-sandbox keepalive.
|
|
231
|
-
//
|
|
232
|
-
// This endpoint is the single thing preventing Sprites' CRIU-based
|
|
233
|
-
// hibernation from freezing osborn's Node.js event loop and dropping our
|
|
234
|
-
// LiveKit WebSocket mid-session. Short HTTP pings don't work: Sprites'
|
|
235
|
-
// warm state serves /health responses from a process snapshot without
|
|
236
|
-
// actually resuming the event loop, so background timers (including
|
|
237
|
-
// LiveKit heartbeats) stop firing after a few seconds. That causes the
|
|
238
|
-
// LiveKit server to drop osborn's participant, delete the room, and
|
|
239
|
-
// leave any future user joins stuck at "Connecting..." forever.
|
|
240
|
-
//
|
|
241
|
-
// An OPEN long-lived TCP connection keeps the sprite in 'running' state.
|
|
242
|
-
// The frontend opens this endpoint on chat page mount and holds it open
|
|
243
|
-
// for the entire voice session. While open, osborn's event loop ticks
|
|
244
|
-
// continuously, LiveKit heartbeats fire, and the room stays alive.
|
|
245
|
-
//
|
|
246
|
-
// For local (non-cloud) dev, this endpoint is harmless — it just idles
|
|
247
|
-
// on a client that may never connect. Zero cost when unused.
|
|
248
|
-
if (req.method === 'GET' && url.pathname === '/events') {
|
|
249
|
-
res.writeHead(200, {
|
|
250
|
-
'Content-Type': 'text/event-stream',
|
|
251
|
-
'Cache-Control': 'no-cache',
|
|
252
|
-
'Connection': 'keep-alive',
|
|
253
|
-
// Disable proxy buffering (nginx-style) so each ping is flushed
|
|
254
|
-
// through Sprites' reverse proxy immediately rather than batched.
|
|
255
|
-
'X-Accel-Buffering': 'no',
|
|
256
|
-
});
|
|
257
|
-
res.write(`: sprite-keepalive connected at ${new Date().toISOString()}\n\n`);
|
|
258
|
-
const heartbeat = setInterval(() => {
|
|
259
|
-
try {
|
|
260
|
-
res.write(`: ping ${Date.now()}\n\n`);
|
|
261
|
-
}
|
|
262
|
-
catch { }
|
|
263
|
-
}, 10_000);
|
|
264
|
-
req.on('close', () => {
|
|
265
|
-
clearInterval(heartbeat);
|
|
266
|
-
console.log('[events] SSE client disconnected');
|
|
267
|
-
});
|
|
268
|
-
console.log('[events] SSE client connected');
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
230
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
272
231
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
273
232
|
});
|
|
@@ -447,43 +406,6 @@ async function main() {
|
|
|
447
406
|
let currentSession = null;
|
|
448
407
|
let currentAgent = null; // For updateChatCtx() context injection
|
|
449
408
|
let currentLLM = null;
|
|
450
|
-
/**
|
|
451
|
-
* Hard-kill the in-flight Claude SDK query AND the persistent subprocess.
|
|
452
|
-
*
|
|
453
|
-
* Why this exists: the persistent ClaudeLLM session is deliberately kept alive
|
|
454
|
-
* across user messages to avoid JSONL replay (see CLAUDE.md "Persistent Session
|
|
455
|
-
* Architecture"). When the participant disconnects, simply nulling `currentLLM`
|
|
456
|
-
* drops the JS reference but does NOT kill the underlying Claude Code subprocess
|
|
457
|
-
* — the SDK keeps draining the MessageChannel, running tools, and pushing TTS
|
|
458
|
-
* calls into a now-null voice session. Visible in logs as repeated:
|
|
459
|
-
* "⚠️ tts_say fired but currentSession is null — text dropped"
|
|
460
|
-
* followed by orphaned `🔧 Claude: Bash` calls and `📍 Checkpoint captured` lines
|
|
461
|
-
* that nobody is listening to. Wasted compute, wasted tokens, possible side effects.
|
|
462
|
-
*
|
|
463
|
-
* The right cleanup is `abortQuery()` (on ClaudeLLM directly) or `abortAgent()`
|
|
464
|
-
* (on PipelineDirectLLM, which wraps ClaudeLLM). They both call into
|
|
465
|
-
* `closeSession()` → kills the subprocess. We duck-type to handle both class
|
|
466
|
-
* shapes since `currentLLM` can hold either, depending on voice mode.
|
|
467
|
-
*/
|
|
468
|
-
function killCurrentLLM(reason) {
|
|
469
|
-
if (!currentLLM)
|
|
470
|
-
return;
|
|
471
|
-
try {
|
|
472
|
-
const llm = currentLLM;
|
|
473
|
-
if (typeof llm.abortQuery === 'function') {
|
|
474
|
-
llm.abortQuery();
|
|
475
|
-
}
|
|
476
|
-
else if (typeof llm.abortAgent === 'function') {
|
|
477
|
-
llm.abortAgent();
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
console.warn(`⚠️ killCurrentLLM(${reason}): no abort method on currentLLM`);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
catch (err) {
|
|
484
|
-
console.error(`❌ killCurrentLLM(${reason}) failed:`, err instanceof Error ? err.message : err);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
409
|
let localParticipant = null;
|
|
488
410
|
let agentState = 'initializing';
|
|
489
411
|
// Session-level always-allow list: paths the user has approved for this session without prompting
|
|
@@ -557,7 +479,7 @@ async function main() {
|
|
|
557
479
|
// fullText is what was being spoken when interrupted (passed from tts_say handler).
|
|
558
480
|
// No word-level cutoff for say() — only generateReply pipeline has that — but Claude
|
|
559
481
|
// knows its own output from JSONL, so the full block is enough context.
|
|
560
|
-
console.log(`🔇 Speech interrupted. Was speaking
|
|
482
|
+
console.log(`🔇 Speech interrupted. Was speaking: "${fullText.substring(0, 80)}..."`);
|
|
561
483
|
// Read last 10 assistant messages from JSONL (Claude's full untruncated output).
|
|
562
484
|
// SessionMessage.text is pre-joined from all text content blocks.
|
|
563
485
|
let recentMessages = '';
|
|
@@ -933,7 +855,7 @@ async function main() {
|
|
|
933
855
|
});
|
|
934
856
|
// Wire up Claude text output - RAW text goes to frontend for chat bubbles
|
|
935
857
|
directLLM.events.on('assistant_text', (data) => {
|
|
936
|
-
console.log(`💬 Claude text
|
|
858
|
+
console.log(`💬 Claude text: ${data.text?.substring(0, 60)}...`);
|
|
937
859
|
sendToFrontend({
|
|
938
860
|
type: 'claude_output',
|
|
939
861
|
text: data.text,
|
|
@@ -943,7 +865,7 @@ async function main() {
|
|
|
943
865
|
});
|
|
944
866
|
// Wire up Claude final result - RAW result goes to frontend
|
|
945
867
|
directLLM.events.on('assistant_result', (data) => {
|
|
946
|
-
console.log(`📋 Claude result
|
|
868
|
+
console.log(`📋 Claude result: ${data.text?.substring(0, 60)}...`);
|
|
947
869
|
sendToFrontend({
|
|
948
870
|
type: 'claude_output',
|
|
949
871
|
text: data.text,
|
|
@@ -1061,7 +983,7 @@ async function main() {
|
|
|
1061
983
|
directLLM.events.on('tts_say', (data) => {
|
|
1062
984
|
// Guard: session must be alive — TTS errors can kill the session while background query runs
|
|
1063
985
|
if (!currentSession) {
|
|
1064
|
-
console.warn(`⚠️ tts_say fired but currentSession is null — text dropped
|
|
986
|
+
console.warn(`⚠️ tts_say fired but currentSession is null — text dropped: "${data.text?.substring(0, 60)}"`);
|
|
1065
987
|
return;
|
|
1066
988
|
}
|
|
1067
989
|
if (!data.text?.trim()) {
|
|
@@ -1069,7 +991,7 @@ async function main() {
|
|
|
1069
991
|
return;
|
|
1070
992
|
}
|
|
1071
993
|
const sayId = Date.now(); // simple ID to correlate start/end logs
|
|
1072
|
-
console.log(`🗣️ [${sayId}] session.say START (${data.text.length} chars): "${data.text}"`);
|
|
994
|
+
console.log(`🗣️ [${sayId}] session.say START (${data.text.length} chars): "${data.text.substring(0, 60)}..."`);
|
|
1073
995
|
try {
|
|
1074
996
|
const handle = currentSession.say(data.text);
|
|
1075
997
|
if (handle && typeof handle.addDoneCallback === 'function') {
|
|
@@ -1197,7 +1119,7 @@ async function main() {
|
|
|
1197
1119
|
}
|
|
1198
1120
|
});
|
|
1199
1121
|
realtimeClaudeHandler.events.on('assistant_result', (data) => {
|
|
1200
|
-
console.log(`📋 Claude result
|
|
1122
|
+
console.log(`📋 Claude result: ${data.text?.substring(0, 60)}...`);
|
|
1201
1123
|
sendToFrontend({
|
|
1202
1124
|
type: 'claude_output',
|
|
1203
1125
|
text: data.text,
|
|
@@ -1689,9 +1611,6 @@ async function main() {
|
|
|
1689
1611
|
lastCompletedResearch = null;
|
|
1690
1612
|
currentSession = null;
|
|
1691
1613
|
currentAgent = null;
|
|
1692
|
-
// Same disconnect-leak fix as the other two cleanup sites — kill the Claude SDK
|
|
1693
|
-
// subprocess BEFORE dropping the reference. See killCurrentLLM() for full context.
|
|
1694
|
-
killCurrentLLM('disconnected_cleanup');
|
|
1695
1614
|
currentLLM = null;
|
|
1696
1615
|
clearFastBrainSession();
|
|
1697
1616
|
clearPipelineFastBrainSession();
|
|
@@ -1734,9 +1653,6 @@ async function main() {
|
|
|
1734
1653
|
catch { }
|
|
1735
1654
|
currentSession = null;
|
|
1736
1655
|
currentAgent = null;
|
|
1737
|
-
// Same disconnect-leak fix — kill the previous user's Claude subprocess
|
|
1738
|
-
// before binding currentLLM to the new user's session below.
|
|
1739
|
-
killCurrentLLM('previous_session_cleanup');
|
|
1740
1656
|
currentLLM = null;
|
|
1741
1657
|
}
|
|
1742
1658
|
// Extract voice architecture, provider, and sessionId from participant metadata (sent by frontend)
|
|
@@ -1905,7 +1821,7 @@ async function main() {
|
|
|
1905
1821
|
// (Gemini v1.0.51: userInput in generateReply creates a user conversation item)
|
|
1906
1822
|
if (normalized.startsWith('[SCRIPT]') || normalized.startsWith('[PROACTIVE]') || normalized.startsWith('[NOTIFICATION]'))
|
|
1907
1823
|
return;
|
|
1908
|
-
console.log(`📝 User (${source}
|
|
1824
|
+
console.log(`📝 User (${source}): "${transcript.substring(0, 60)}..."`);
|
|
1909
1825
|
sendToFrontend({ type: 'user_transcript', text: transcript });
|
|
1910
1826
|
lastSentUserTranscript = normalized;
|
|
1911
1827
|
}
|
|
@@ -1915,7 +1831,7 @@ async function main() {
|
|
|
1915
1831
|
const normalized = text.trim().replace(/\s+/g, ' ');
|
|
1916
1832
|
if (normalized === lastSentAgentTranscript)
|
|
1917
1833
|
return;
|
|
1918
|
-
console.log(`💬 Agent (${source}
|
|
1834
|
+
console.log(`💬 Agent (${source}): "${text.substring(0, 60)}..."`);
|
|
1919
1835
|
sendToFrontend({ type: 'assistant_response', text });
|
|
1920
1836
|
lastSentAgentTranscript = normalized;
|
|
1921
1837
|
}
|
|
@@ -2373,9 +2289,6 @@ async function main() {
|
|
|
2373
2289
|
})();
|
|
2374
2290
|
}
|
|
2375
2291
|
currentAgent = null;
|
|
2376
|
-
// Kill the Claude SDK subprocess BEFORE dropping the reference, otherwise the
|
|
2377
|
-
// persistent session keeps running tools and pushing TTS into a dead session.
|
|
2378
|
-
killCurrentLLM('participant_disconnected');
|
|
2379
2292
|
currentLLM = null;
|
|
2380
2293
|
clearFastBrainSession();
|
|
2381
2294
|
clearPipelineFastBrainSession();
|
|
@@ -2421,10 +2334,10 @@ async function main() {
|
|
|
2421
2334
|
fullContent += `\n\n[Image attached: ${f.name}]`;
|
|
2422
2335
|
}
|
|
2423
2336
|
}
|
|
2424
|
-
console.log(`📝 Text + ${files.length} file(s)
|
|
2337
|
+
console.log(`📝 Text + ${files.length} file(s): "${fullContent.substring(0, 100)}"`);
|
|
2425
2338
|
}
|
|
2426
2339
|
else {
|
|
2427
|
-
console.log(`📝 Text
|
|
2340
|
+
console.log(`📝 Text: "${fullContent.substring(0, 100)}"`);
|
|
2428
2341
|
}
|
|
2429
2342
|
// Skip interrupt for Gemini — disrupts state machine (hangs in speaking state)
|
|
2430
2343
|
if (currentProvider !== 'gemini') {
|
|
@@ -82,7 +82,7 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
82
82
|
break;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
console.log(`📥 [pipeline] chat() call #${callN}
|
|
85
|
+
console.log(`📥 [pipeline] chat() call #${callN}: "${userText.substring(0, 60)}"`);
|
|
86
86
|
// Check for pending interruption context — enrich user message if interrupted
|
|
87
87
|
const interruptCtx = this.#opts.getAndConsumeInterruptionContext?.();
|
|
88
88
|
if (interruptCtx && userText.trim()) {
|