osborn 0.8.7 → 0.8.9
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 -56
- 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
|
@@ -447,43 +447,6 @@ async function main() {
|
|
|
447
447
|
let currentSession = null;
|
|
448
448
|
let currentAgent = null; // For updateChatCtx() context injection
|
|
449
449
|
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
450
|
let localParticipant = null;
|
|
488
451
|
let agentState = 'initializing';
|
|
489
452
|
// Session-level always-allow list: paths the user has approved for this session without prompting
|
|
@@ -557,7 +520,7 @@ async function main() {
|
|
|
557
520
|
// fullText is what was being spoken when interrupted (passed from tts_say handler).
|
|
558
521
|
// No word-level cutoff for say() — only generateReply pipeline has that — but Claude
|
|
559
522
|
// knows its own output from JSONL, so the full block is enough context.
|
|
560
|
-
console.log(`🔇 Speech interrupted. Was speaking
|
|
523
|
+
console.log(`🔇 Speech interrupted. Was speaking: "${fullText.substring(0, 80)}..."`);
|
|
561
524
|
// Read last 10 assistant messages from JSONL (Claude's full untruncated output).
|
|
562
525
|
// SessionMessage.text is pre-joined from all text content blocks.
|
|
563
526
|
let recentMessages = '';
|
|
@@ -933,7 +896,7 @@ async function main() {
|
|
|
933
896
|
});
|
|
934
897
|
// Wire up Claude text output - RAW text goes to frontend for chat bubbles
|
|
935
898
|
directLLM.events.on('assistant_text', (data) => {
|
|
936
|
-
console.log(`💬 Claude text
|
|
899
|
+
console.log(`💬 Claude text: ${data.text?.substring(0, 60)}...`);
|
|
937
900
|
sendToFrontend({
|
|
938
901
|
type: 'claude_output',
|
|
939
902
|
text: data.text,
|
|
@@ -943,7 +906,7 @@ async function main() {
|
|
|
943
906
|
});
|
|
944
907
|
// Wire up Claude final result - RAW result goes to frontend
|
|
945
908
|
directLLM.events.on('assistant_result', (data) => {
|
|
946
|
-
console.log(`📋 Claude result
|
|
909
|
+
console.log(`📋 Claude result: ${data.text?.substring(0, 60)}...`);
|
|
947
910
|
sendToFrontend({
|
|
948
911
|
type: 'claude_output',
|
|
949
912
|
text: data.text,
|
|
@@ -1061,7 +1024,7 @@ async function main() {
|
|
|
1061
1024
|
directLLM.events.on('tts_say', (data) => {
|
|
1062
1025
|
// Guard: session must be alive — TTS errors can kill the session while background query runs
|
|
1063
1026
|
if (!currentSession) {
|
|
1064
|
-
console.warn(`⚠️ tts_say fired but currentSession is null — text dropped
|
|
1027
|
+
console.warn(`⚠️ tts_say fired but currentSession is null — text dropped: "${data.text?.substring(0, 60)}"`);
|
|
1065
1028
|
return;
|
|
1066
1029
|
}
|
|
1067
1030
|
if (!data.text?.trim()) {
|
|
@@ -1069,7 +1032,7 @@ async function main() {
|
|
|
1069
1032
|
return;
|
|
1070
1033
|
}
|
|
1071
1034
|
const sayId = Date.now(); // simple ID to correlate start/end logs
|
|
1072
|
-
console.log(`🗣️ [${sayId}] session.say START (${data.text.length} chars): "${data.text}"`);
|
|
1035
|
+
console.log(`🗣️ [${sayId}] session.say START (${data.text.length} chars): "${data.text.substring(0, 60)}..."`);
|
|
1073
1036
|
try {
|
|
1074
1037
|
const handle = currentSession.say(data.text);
|
|
1075
1038
|
if (handle && typeof handle.addDoneCallback === 'function') {
|
|
@@ -1197,7 +1160,7 @@ async function main() {
|
|
|
1197
1160
|
}
|
|
1198
1161
|
});
|
|
1199
1162
|
realtimeClaudeHandler.events.on('assistant_result', (data) => {
|
|
1200
|
-
console.log(`📋 Claude result
|
|
1163
|
+
console.log(`📋 Claude result: ${data.text?.substring(0, 60)}...`);
|
|
1201
1164
|
sendToFrontend({
|
|
1202
1165
|
type: 'claude_output',
|
|
1203
1166
|
text: data.text,
|
|
@@ -1689,9 +1652,6 @@ async function main() {
|
|
|
1689
1652
|
lastCompletedResearch = null;
|
|
1690
1653
|
currentSession = null;
|
|
1691
1654
|
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
1655
|
currentLLM = null;
|
|
1696
1656
|
clearFastBrainSession();
|
|
1697
1657
|
clearPipelineFastBrainSession();
|
|
@@ -1734,9 +1694,6 @@ async function main() {
|
|
|
1734
1694
|
catch { }
|
|
1735
1695
|
currentSession = null;
|
|
1736
1696
|
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
1697
|
currentLLM = null;
|
|
1741
1698
|
}
|
|
1742
1699
|
// Extract voice architecture, provider, and sessionId from participant metadata (sent by frontend)
|
|
@@ -1905,7 +1862,7 @@ async function main() {
|
|
|
1905
1862
|
// (Gemini v1.0.51: userInput in generateReply creates a user conversation item)
|
|
1906
1863
|
if (normalized.startsWith('[SCRIPT]') || normalized.startsWith('[PROACTIVE]') || normalized.startsWith('[NOTIFICATION]'))
|
|
1907
1864
|
return;
|
|
1908
|
-
console.log(`📝 User (${source}
|
|
1865
|
+
console.log(`📝 User (${source}): "${transcript.substring(0, 60)}..."`);
|
|
1909
1866
|
sendToFrontend({ type: 'user_transcript', text: transcript });
|
|
1910
1867
|
lastSentUserTranscript = normalized;
|
|
1911
1868
|
}
|
|
@@ -1915,7 +1872,7 @@ async function main() {
|
|
|
1915
1872
|
const normalized = text.trim().replace(/\s+/g, ' ');
|
|
1916
1873
|
if (normalized === lastSentAgentTranscript)
|
|
1917
1874
|
return;
|
|
1918
|
-
console.log(`💬 Agent (${source}
|
|
1875
|
+
console.log(`💬 Agent (${source}): "${text.substring(0, 60)}..."`);
|
|
1919
1876
|
sendToFrontend({ type: 'assistant_response', text });
|
|
1920
1877
|
lastSentAgentTranscript = normalized;
|
|
1921
1878
|
}
|
|
@@ -2373,9 +2330,6 @@ async function main() {
|
|
|
2373
2330
|
})();
|
|
2374
2331
|
}
|
|
2375
2332
|
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
2333
|
currentLLM = null;
|
|
2380
2334
|
clearFastBrainSession();
|
|
2381
2335
|
clearPipelineFastBrainSession();
|
|
@@ -2421,10 +2375,10 @@ async function main() {
|
|
|
2421
2375
|
fullContent += `\n\n[Image attached: ${f.name}]`;
|
|
2422
2376
|
}
|
|
2423
2377
|
}
|
|
2424
|
-
console.log(`📝 Text + ${files.length} file(s)
|
|
2378
|
+
console.log(`📝 Text + ${files.length} file(s): "${fullContent.substring(0, 100)}"`);
|
|
2425
2379
|
}
|
|
2426
2380
|
else {
|
|
2427
|
-
console.log(`📝 Text
|
|
2381
|
+
console.log(`📝 Text: "${fullContent.substring(0, 100)}"`);
|
|
2428
2382
|
}
|
|
2429
2383
|
// Skip interrupt for Gemini — disrupts state machine (hangs in speaking state)
|
|
2430
2384
|
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()) {
|