osborn 0.9.131 → 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/claude-llm.js +4 -3
- package/dist/index.js +180 -15
- package/dist/pipeline-direct-llm.d.ts +0 -2
- package/dist/pipeline-direct-llm.js +1 -4
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -1132,7 +1132,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1132
1132
|
// Writer agent: no longer auto-approved — falls through to canUseTool for permission dialog
|
|
1133
1133
|
if (agentType === 'writer') {
|
|
1134
1134
|
console.log(`✍️ Writer agent: deferring to canUseTool for permission`);
|
|
1135
|
-
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput });
|
|
1135
|
+
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
|
|
1136
1136
|
return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask' } };
|
|
1137
1137
|
}
|
|
1138
1138
|
// All other agents (main, researcher, reasoner, etc.): workspace only
|
|
@@ -1144,7 +1144,7 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1144
1144
|
}
|
|
1145
1145
|
}
|
|
1146
1146
|
console.log(`🔧 Claude: ${toolName}`);
|
|
1147
|
-
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput });
|
|
1147
|
+
this.#eventEmitter.emit('tool_use', { name: toolName, input: toolInput, agentRole: agentType || 'main' });
|
|
1148
1148
|
return {};
|
|
1149
1149
|
}]
|
|
1150
1150
|
}],
|
|
@@ -1154,8 +1154,9 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1154
1154
|
const toolName = input?.tool_name || 'unknown';
|
|
1155
1155
|
const toolInput = input?.tool_input || {};
|
|
1156
1156
|
const toolResponse = input?.tool_response; // Capture actual tool output for fast brain processing
|
|
1157
|
+
const agentTypePost = input?.agent_type || null;
|
|
1157
1158
|
console.log(`✅ Done: ${toolName}`);
|
|
1158
|
-
this.#eventEmitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse });
|
|
1159
|
+
this.#eventEmitter.emit('tool_result', { name: toolName, input: toolInput, response: toolResponse, agentRole: agentTypePost || 'main' });
|
|
1159
1160
|
return {};
|
|
1160
1161
|
}]
|
|
1161
1162
|
}],
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
19
|
// 0.9.71: createRequire for resolving package.json versions inside ESM
|
|
20
20
|
const __sdkVersionRequire = createRequire(import.meta.url);
|
|
21
|
-
import { homedir, tmpdir } from 'node:os';
|
|
21
|
+
import { homedir, tmpdir, totalmem, freemem } from 'node:os';
|
|
22
22
|
import { PassThrough } from 'node:stream';
|
|
23
23
|
import { createGunzip } from 'node:zlib';
|
|
24
24
|
// Resolve __dirname for this ESM module so we can find sibling files (e.g.
|
|
@@ -518,6 +518,15 @@ function startApiServer(workingDir, port) {
|
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
520
|
catch { /* version optional */ }
|
|
521
|
+
// System memory snapshot — use os.totalmem/freemem for WHOLE-MACHINE
|
|
522
|
+
// stats (the OOM killer watches these, not per-process RSS). processRssMb
|
|
523
|
+
// is additive: useful for spotting leaks but never the headline number.
|
|
524
|
+
const totalMb = Math.round(totalmem() / 1024 / 1024);
|
|
525
|
+
const freeMb = Math.round(freemem() / 1024 / 1024);
|
|
526
|
+
const usedMb = totalMb - freeMb;
|
|
527
|
+
const availableMb = freeMb;
|
|
528
|
+
const usedPct = Math.round((usedMb / totalMb) * 100);
|
|
529
|
+
const processRssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
521
530
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
522
531
|
res.end(JSON.stringify({
|
|
523
532
|
status: 'ok',
|
|
@@ -538,6 +547,8 @@ function startApiServer(workingDir, port) {
|
|
|
538
547
|
attemptCount: livekitState.attemptCount,
|
|
539
548
|
lastAttemptAt: livekitState.lastAttemptAt,
|
|
540
549
|
},
|
|
550
|
+
// Whole-machine memory — what the OOM killer watches. All values in MB.
|
|
551
|
+
memory: { totalMb, usedMb, availableMb, usedPct, processRssMb },
|
|
541
552
|
}));
|
|
542
553
|
return;
|
|
543
554
|
}
|
|
@@ -1678,6 +1689,8 @@ async function main() {
|
|
|
1678
1689
|
let participantConnectedHandler = null;
|
|
1679
1690
|
let currentAgent = null; // For updateChatCtx() context injection
|
|
1680
1691
|
let currentLLM = null;
|
|
1692
|
+
const slots = new Map();
|
|
1693
|
+
let focusedSlotId = null; // reserved for future focused-slot wiring; not used yet
|
|
1681
1694
|
// Agent-side "alone in room" leave timer (see Room-presence lifecycle note up
|
|
1682
1695
|
// top). Armed in ParticipantDisconnected once a user has left; if no one
|
|
1683
1696
|
// rejoins within the grace window the agent leaves LiveKit on its own.
|
|
@@ -1846,6 +1859,100 @@ async function main() {
|
|
|
1846
1859
|
console.error(`❌ killCurrentLLM(${reason}) failed:`, err instanceof Error ? err.message : err);
|
|
1847
1860
|
}
|
|
1848
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
|
+
}
|
|
1849
1956
|
let localParticipant = null;
|
|
1850
1957
|
let agentState = 'initializing';
|
|
1851
1958
|
// Session-level always-allow list: paths the user has approved for this session without prompting
|
|
@@ -2214,7 +2321,7 @@ async function main() {
|
|
|
2214
2321
|
const { readSessionHistory } = await import('./session-access.js');
|
|
2215
2322
|
const history = readSessionHistory(sessionId, workingDir, {
|
|
2216
2323
|
lastN: 10,
|
|
2217
|
-
types: ['assistant'
|
|
2324
|
+
types: ['assistant'],
|
|
2218
2325
|
});
|
|
2219
2326
|
recentMessages = history
|
|
2220
2327
|
.filter((m) => m.text)
|
|
@@ -2230,14 +2337,8 @@ async function main() {
|
|
|
2230
2337
|
// BEFORE the previous TTS completed, and we may have already suppressed in-flight
|
|
2231
2338
|
// tts_say events that arrived during that overlap).
|
|
2232
2339
|
const carriedSuppressed = lastInterruption?.suppressedText ?? '';
|
|
2233
|
-
lastInterruption = {
|
|
2234
|
-
|
|
2235
|
-
recentMessages,
|
|
2236
|
-
suppressedText: carriedSuppressed,
|
|
2237
|
-
fullBlockText: fullBlockText && fullBlockText !== fullText ? fullBlockText : undefined,
|
|
2238
|
-
timestamp: Date.now(),
|
|
2239
|
-
};
|
|
2240
|
-
console.log(`📋 Interruption context stored (text: ${fullText.length} chars, JSONL: ${recentMessages.length} chars, block: ${fullBlockText?.length ?? 0} chars, suppressed carried: ${carriedSuppressed.length} chars)`);
|
|
2340
|
+
lastInterruption = { spokenText: fullText, recentMessages, suppressedText: carriedSuppressed, timestamp: Date.now() };
|
|
2341
|
+
console.log(`📋 Interruption context stored (text: ${fullText.length} chars, JSONL: ${recentMessages.length} chars, suppressed carried: ${carriedSuppressed.length} chars)`);
|
|
2241
2342
|
}
|
|
2242
2343
|
/**
|
|
2243
2344
|
* Append text the agent tried to say while the user was speaking, but which we
|
|
@@ -2277,7 +2378,6 @@ async function main() {
|
|
|
2277
2378
|
spokenText: lastInterruption.spokenText,
|
|
2278
2379
|
recentMessages: lastInterruption.recentMessages,
|
|
2279
2380
|
suppressedText: lastInterruption.suppressedText,
|
|
2280
|
-
fullBlockText: lastInterruption.fullBlockText,
|
|
2281
2381
|
};
|
|
2282
2382
|
lastInterruption = null;
|
|
2283
2383
|
return ctx;
|
|
@@ -2689,11 +2789,11 @@ async function main() {
|
|
|
2689
2789
|
// Wire up events from the Claude SDK wrapper to frontend
|
|
2690
2790
|
directLLM.events.on('tool_use', (data) => {
|
|
2691
2791
|
console.log(`🔧 Claude: ${data.name}`);
|
|
2692
|
-
sendToFrontend(buildToolLogEvent(data.name, data.input, 'running', '
|
|
2792
|
+
sendToFrontend(buildToolLogEvent(data.name, data.input, 'running', data.agentRole || 'main'));
|
|
2693
2793
|
});
|
|
2694
2794
|
directLLM.events.on('tool_result', (data) => {
|
|
2695
2795
|
console.log(`✅ Done: ${data.name}`);
|
|
2696
|
-
sendToFrontend(buildToolLogEvent(data.name, data.input, 'completed', '
|
|
2796
|
+
sendToFrontend(buildToolLogEvent(data.name, data.input, 'completed', data.agentRole || 'main'));
|
|
2697
2797
|
// Detect research artifact writes (session workspace or legacy research dir)
|
|
2698
2798
|
if ((data.name === 'Write' || data.name === 'Edit') && data.input?.file_path) {
|
|
2699
2799
|
const fp = data.input.file_path;
|
|
@@ -3128,11 +3228,11 @@ async function main() {
|
|
|
3128
3228
|
// Wire up Claude events to frontend
|
|
3129
3229
|
realtimeClaudeHandler.events.on('tool_use', (data) => {
|
|
3130
3230
|
console.log(`🔧 Claude: ${data.name}`);
|
|
3131
|
-
sendToFrontend(buildToolLogEvent(data.name, data.input, 'running', '
|
|
3231
|
+
sendToFrontend(buildToolLogEvent(data.name, data.input, 'running', data.agentRole || 'main'));
|
|
3132
3232
|
});
|
|
3133
3233
|
realtimeClaudeHandler.events.on('tool_result', (data) => {
|
|
3134
3234
|
console.log(`✅ Done: ${data.name}`);
|
|
3135
|
-
sendToFrontend(buildToolLogEvent(data.name, data.input, 'completed', '
|
|
3235
|
+
sendToFrontend(buildToolLogEvent(data.name, data.input, 'completed', data.agentRole || 'main'));
|
|
3136
3236
|
// Detect research artifact writes (session workspace or legacy research dir)
|
|
3137
3237
|
if ((data.name === 'Write' || data.name === 'Edit') && data.input?.file_path) {
|
|
3138
3238
|
const fp = data.input.file_path;
|
|
@@ -5603,6 +5703,71 @@ async function main() {
|
|
|
5603
5703
|
}
|
|
5604
5704
|
}
|
|
5605
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
|
+
}
|
|
5606
5771
|
}
|
|
5607
5772
|
catch { }
|
|
5608
5773
|
};
|
|
@@ -21,8 +21,6 @@ export interface InterruptionContext {
|
|
|
21
21
|
* and can re-articulate the relevant bits in its next response.
|
|
22
22
|
*/
|
|
23
23
|
suppressedText: string;
|
|
24
|
-
/** Full text of the interrupted TTS block — fallback when JSONL read returns empty */
|
|
25
|
-
fullBlockText?: string;
|
|
26
24
|
}
|
|
27
25
|
export interface PipelineDirectOptions extends ClaudeLLMOptions {
|
|
28
26
|
onFastBrainResult?: (result: FastBrainPanelResult) => void;
|
|
@@ -122,10 +122,7 @@ export class PipelineDirectLLM extends llm.LLM {
|
|
|
122
122
|
`Anything in "Your recent messages" below that appears AFTER the quoted heard text is content the user did not hear. The user has no memory of it.`,
|
|
123
123
|
``,
|
|
124
124
|
`Your recent messages (full untruncated — you wrote these):`,
|
|
125
|
-
interruptCtx.recentMessages
|
|
126
|
-
|| (interruptCtx.fullBlockText
|
|
127
|
-
? `(JSONL unavailable — full TTS block text follows)\n"${interruptCtx.fullBlockText}"`
|
|
128
|
-
: '(no recent messages found)'),
|
|
125
|
+
interruptCtx.recentMessages || '(no recent messages found)',
|
|
129
126
|
suppressedBlock,
|
|
130
127
|
``,
|
|
131
128
|
`User's message: "${userText}"`,
|