osborn 0.8.0 → 0.8.2
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/bin/cli.js +25 -9
- package/dist/claude-auth.d.ts +2 -2
- package/dist/claude-auth.js +61 -6
- package/dist/claude-llm.js +11 -15
- package/dist/config.d.ts +35 -13
- package/dist/config.js +146 -39
- package/dist/fast-brain.d.ts +6 -6
- package/dist/fast-brain.js +17 -97
- package/dist/index.js +81 -51
- package/dist/pipeline-direct-llm.js +2 -2
- package/dist/pipeline-fastbrain.js +10 -9
- package/dist/prompts.d.ts +4 -4
- package/dist/prompts.js +28 -57
- package/dist/session-access.d.ts +1 -0
- package/dist/session-access.js +1 -1
- package/dist/summary-index.d.ts +8 -5
- package/dist/summary-index.js +28 -13
- package/package.json +1 -1
package/dist/fast-brain.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* The realtime voice model is a thin teleprompter — it speaks what this module returns.
|
|
6
6
|
*
|
|
7
7
|
* Capabilities:
|
|
8
|
-
* - Read/write session files (spec.md
|
|
8
|
+
* - Read/write session files (spec.md)
|
|
9
9
|
* - Web search for quick factual lookups
|
|
10
10
|
* - Record user decisions and preferences into spec.md
|
|
11
11
|
* - Trigger deep research (via callbacks to index.ts)
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
import Anthropic from '@anthropic-ai/sdk';
|
|
25
25
|
import { query as sdkQuery, tool as sdkTool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
|
|
26
26
|
import { GoogleGenAI } from '@google/genai';
|
|
27
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync
|
|
27
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
28
28
|
import { dirname, basename } from 'path';
|
|
29
29
|
import { z } from 'zod';
|
|
30
|
-
import { getSessionWorkspace, readSessionSpec
|
|
30
|
+
import { getSessionWorkspace, readSessionSpec } from './config.js';
|
|
31
31
|
import { FAST_BRAIN_SYSTEM_PROMPT, CHUNK_PROCESS_SYSTEM, REFINEMENT_PROCESS_SYSTEM, AUGMENT_RESULT_SYSTEM, CONTEXTUALIZE_UPDATE_SYSTEM, PROACTIVE_PROMPT_SYSTEM, VISUAL_DOCUMENT_SYSTEM, RESEARCH_COMPLETION_SYSTEM, buildFastBrainSdkPrompt } from './prompts.js';
|
|
32
32
|
import { getRecentToolResults, readSessionHistory, getSubagentTranscripts, getConversationText, getSessionTranscripts, searchSessionJsonl, getSessionStats } from './session-access.js';
|
|
33
33
|
// ============================================================
|
|
@@ -164,18 +164,6 @@ function executeTool(toolName, toolInput, workspace, sessionId, workingDir, send
|
|
|
164
164
|
console.log(`📝 Fast brain wrote ${relPath} (${content.length} chars)`);
|
|
165
165
|
return `Written: ${relPath} (${content.length} chars)`;
|
|
166
166
|
}
|
|
167
|
-
case 'list_library': {
|
|
168
|
-
const libraryDir = `${workspace}/library`;
|
|
169
|
-
if (!existsSync(libraryDir))
|
|
170
|
-
return 'Library is empty — no research files yet.';
|
|
171
|
-
try {
|
|
172
|
-
const items = readdirSync(libraryDir);
|
|
173
|
-
return items.length > 0 ? items.join('\n') : 'Library is empty — no research files yet.';
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
return 'Library is empty — no research files yet.';
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
167
|
case 'read_agent_results': {
|
|
180
168
|
if (!sessionId || !workingDir)
|
|
181
169
|
return 'Error: no active research session';
|
|
@@ -328,7 +316,7 @@ function buildAnthropicTools() {
|
|
|
328
316
|
return [
|
|
329
317
|
{
|
|
330
318
|
name: 'read_file',
|
|
331
|
-
description: 'Read a file from the session workspace. Use
|
|
319
|
+
description: 'Read a file from the session workspace. Use "spec.md" to read the session spec.',
|
|
332
320
|
input_schema: {
|
|
333
321
|
type: 'object',
|
|
334
322
|
properties: {
|
|
@@ -349,11 +337,6 @@ function buildAnthropicTools() {
|
|
|
349
337
|
required: ['path', 'content']
|
|
350
338
|
}
|
|
351
339
|
},
|
|
352
|
-
{
|
|
353
|
-
name: 'list_library',
|
|
354
|
-
description: 'List all files in the research library directory.',
|
|
355
|
-
input_schema: { type: 'object', properties: {} }
|
|
356
|
-
},
|
|
357
340
|
{
|
|
358
341
|
name: 'read_agent_results',
|
|
359
342
|
description: 'Read the research agent\'s FULL memory — complete untruncated tool outputs including entire file contents the agent read, full bash command outputs, web search results, and web page fetches. This is the agent\'s raw data. Use this FIRST when asked about anything the agent just researched. Default: last 40 results.',
|
|
@@ -460,7 +443,7 @@ function buildGeminiTools() {
|
|
|
460
443
|
functionDeclarations: [
|
|
461
444
|
{
|
|
462
445
|
name: 'read_file',
|
|
463
|
-
description: 'Read a file from the session workspace. Use
|
|
446
|
+
description: 'Read a file from the session workspace. Use "spec.md" to read the session spec.',
|
|
464
447
|
parameters: {
|
|
465
448
|
type: 'object',
|
|
466
449
|
properties: {
|
|
@@ -481,11 +464,6 @@ function buildGeminiTools() {
|
|
|
481
464
|
required: ['path', 'content']
|
|
482
465
|
}
|
|
483
466
|
},
|
|
484
|
-
{
|
|
485
|
-
name: 'list_library',
|
|
486
|
-
description: 'List all files in the research library directory.',
|
|
487
|
-
parameters: { type: 'object', properties: {} }
|
|
488
|
-
},
|
|
489
467
|
{
|
|
490
468
|
name: 'web_search',
|
|
491
469
|
description: 'Search the web for current information. Use for factual questions like "current version of X", "what is X", definitions, etc.',
|
|
@@ -886,7 +864,7 @@ async function askViaGemini(question, workspace, researchContext, sessionId, wor
|
|
|
886
864
|
*/
|
|
887
865
|
export async function askHaiku(workingDir, sessionId, question, researchContext, chatHistory, sendToChat, sessionBaseDir) {
|
|
888
866
|
initProvider();
|
|
889
|
-
// workspace uses
|
|
867
|
+
// workspace uses workingDir for spec.md (under ~/.claude/projects/{slug}/osb/)
|
|
890
868
|
// workingDir is for JSONL access (matches Claude SDK cwd)
|
|
891
869
|
const wsDir = sessionBaseDir || workingDir;
|
|
892
870
|
const workspace = getSessionWorkspace(wsDir, sessionId);
|
|
@@ -917,7 +895,7 @@ export async function askFastBrain(workingDir, sessionId, question, opts) {
|
|
|
917
895
|
try {
|
|
918
896
|
const result = await generateVisualDocument(workingDir, sessionId, question, docMatch, wsDir);
|
|
919
897
|
if (result) {
|
|
920
|
-
const fullPath = `${wsDir
|
|
898
|
+
const fullPath = `${getSessionWorkspace(wsDir, sessionId)}/${result.fileName}`;
|
|
921
899
|
callbacks.sendToFrontend({
|
|
922
900
|
type: 'research_artifact_updated',
|
|
923
901
|
filePath: fullPath,
|
|
@@ -1064,7 +1042,7 @@ function generateResearchAck(question, chatHistory) {
|
|
|
1064
1042
|
// ============================================================
|
|
1065
1043
|
/**
|
|
1066
1044
|
* Process a batch of research content chunks through the fast brain.
|
|
1067
|
-
* Updates spec.md
|
|
1045
|
+
* Updates spec.md incrementally during research.
|
|
1068
1046
|
*
|
|
1069
1047
|
* @param isRefinement - true for the final post-research consolidation pass (higher token budget)
|
|
1070
1048
|
*/
|
|
@@ -1089,30 +1067,8 @@ export async function processResearchChunk(workingDir, sessionId, task, contentC
|
|
|
1089
1067
|
return null;
|
|
1090
1068
|
}
|
|
1091
1069
|
const currentSpec = readFileSync(specPath, 'utf-8');
|
|
1092
|
-
const libraryDir = `${workspace}/library`;
|
|
1093
|
-
// Only read library files during refinement pass (final consolidation)
|
|
1094
|
-
// Mid-research: skip library entirely to stay fast and avoid file proliferation
|
|
1095
|
-
let existingSection = '';
|
|
1096
|
-
if (isRefinement) {
|
|
1097
|
-
const existingFiles = listLibraryFiles(wsDir, sessionId);
|
|
1098
|
-
const existingContents = [];
|
|
1099
|
-
for (const file of existingFiles) {
|
|
1100
|
-
const filePath = `${libraryDir}/${file}`;
|
|
1101
|
-
if (existsSync(filePath)) {
|
|
1102
|
-
try {
|
|
1103
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
1104
|
-
existingContents.push(`--- ${file} ---\n${content}`);
|
|
1105
|
-
}
|
|
1106
|
-
catch { /* skip */ }
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
existingSection = existingContents.length > 0
|
|
1110
|
-
? `\n\nExisting library/ files:\n${existingContents.join('\n\n')}`
|
|
1111
|
-
: '';
|
|
1112
|
-
}
|
|
1113
1070
|
// No content capping — models handle 200K+ tokens (Haiku) / 1M+ (Gemini Flash)
|
|
1114
1071
|
const chunksText = contentChunks.join('\n\n---\n\n');
|
|
1115
|
-
// Use different prompts: mid-research = spec only, refinement = spec + library
|
|
1116
1072
|
const systemPrompt = isRefinement ? REFINEMENT_PROCESS_SYSTEM : CHUNK_PROCESS_SYSTEM;
|
|
1117
1073
|
const userMessage = `Research task: "${task}"
|
|
1118
1074
|
|
|
@@ -1120,7 +1076,6 @@ Current spec.md:
|
|
|
1120
1076
|
\`\`\`markdown
|
|
1121
1077
|
${currentSpec}
|
|
1122
1078
|
\`\`\`
|
|
1123
|
-
${existingSection}
|
|
1124
1079
|
|
|
1125
1080
|
Content chunks from research:
|
|
1126
1081
|
${chunksText}
|
|
@@ -1151,7 +1106,6 @@ Return ONLY valid JSON — no code fences, no explanation.`;
|
|
|
1151
1106
|
if (!parsed)
|
|
1152
1107
|
return null;
|
|
1153
1108
|
let updatedSpec = null;
|
|
1154
|
-
const writtenFiles = [];
|
|
1155
1109
|
// Write spec.md
|
|
1156
1110
|
if (parsed.spec && typeof parsed.spec === 'string' && parsed.spec.length > 50) {
|
|
1157
1111
|
writeFileSync(specPath, parsed.spec, 'utf-8');
|
|
@@ -1159,22 +1113,9 @@ Return ONLY valid JSON — no code fences, no explanation.`;
|
|
|
1159
1113
|
const label = isRefinement ? 'refinement pass' : 'chunk';
|
|
1160
1114
|
console.log(`📋 Fast brain processed research ${label} — spec.md updated (${parsed.spec.length} chars)`);
|
|
1161
1115
|
}
|
|
1162
|
-
// Write library files — ONLY during refinement pass (prevents file proliferation)
|
|
1163
|
-
if (isRefinement && parsed.library && Array.isArray(parsed.library) && parsed.library.length > 0) {
|
|
1164
|
-
mkdirSync(libraryDir, { recursive: true });
|
|
1165
|
-
for (const file of parsed.library) {
|
|
1166
|
-
if (!file.filename || !file.content)
|
|
1167
|
-
continue;
|
|
1168
|
-
const safeName = file.filename.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
1169
|
-
const filePath = `${libraryDir}/${safeName}`;
|
|
1170
|
-
writeFileSync(filePath, file.content, 'utf-8');
|
|
1171
|
-
console.log(`📝 Fast brain wrote library/${safeName} (${file.content.length} chars)`);
|
|
1172
|
-
writtenFiles.push(safeName);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
1116
|
const label = isRefinement ? 'refinement' : `${contentChunks.length} content items`;
|
|
1176
1117
|
console.log(`📋 Fast brain processed research chunk (${label})`);
|
|
1177
|
-
return { spec: updatedSpec, libraryFiles:
|
|
1118
|
+
return { spec: updatedSpec, libraryFiles: [] };
|
|
1178
1119
|
}
|
|
1179
1120
|
catch (err) {
|
|
1180
1121
|
console.error('❌ processResearchChunk failed:', err);
|
|
@@ -1254,18 +1195,14 @@ export async function augmentResearchResult(workingDir, sessionId, task, agentRe
|
|
|
1254
1195
|
try {
|
|
1255
1196
|
// Read spec for context
|
|
1256
1197
|
const specContent = readSessionSpec(workingDir, sessionId);
|
|
1257
|
-
const libraryFiles = listLibraryFiles(workingDir, sessionId);
|
|
1258
1198
|
const specSection = specContent
|
|
1259
1199
|
? `\n\nCurrent spec.md:\n${specContent}`
|
|
1260
1200
|
: '';
|
|
1261
|
-
const libSection = libraryFiles.length > 0
|
|
1262
|
-
? `\n\nLibrary files available: ${libraryFiles.join(', ')}`
|
|
1263
|
-
: '';
|
|
1264
1201
|
const userMessage = `Research task: "${task}"
|
|
1265
1202
|
|
|
1266
1203
|
Agent findings:
|
|
1267
1204
|
${agentResult}
|
|
1268
|
-
${specSection}
|
|
1205
|
+
${specSection}
|
|
1269
1206
|
|
|
1270
1207
|
Augment the agent's findings with relevant context from the spec. Pass ALL details through verbatim.`;
|
|
1271
1208
|
let responseText = null;
|
|
@@ -1303,7 +1240,7 @@ Augment the agent's findings with relevant context from the spec. Pass ALL detai
|
|
|
1303
1240
|
// updateSpecFromJSONL — Post-research spec consolidation via JSONL
|
|
1304
1241
|
// ============================================================
|
|
1305
1242
|
/**
|
|
1306
|
-
* Update spec.md
|
|
1243
|
+
* Update spec.md after research completes.
|
|
1307
1244
|
* Reads FULL untruncated data directly from Claude Agent SDK JSONL files
|
|
1308
1245
|
* instead of receiving pre-truncated content chunks.
|
|
1309
1246
|
*
|
|
@@ -1312,7 +1249,7 @@ Augment the agent's findings with relevant context from the spec. Pass ALL detai
|
|
|
1312
1249
|
* - readSessionHistory() — last 50 assistant messages (agent reasoning/analysis)
|
|
1313
1250
|
* - getSubagentTranscripts() — all sub-agent findings
|
|
1314
1251
|
*
|
|
1315
|
-
* Returns { spec
|
|
1252
|
+
* Returns { spec } or null if update failed.
|
|
1316
1253
|
*/
|
|
1317
1254
|
export async function updateSpecFromJSONL(workingDir, sessionId, task, researchLog, sessionBaseDir) {
|
|
1318
1255
|
initProvider();
|
|
@@ -1692,8 +1629,8 @@ ${previousPrompts.length > 0 ? previousPrompts.join('\n') : '(none yet)'}`;
|
|
|
1692
1629
|
* Generate a structured visual document (comparison table, Mermaid diagram,
|
|
1693
1630
|
* analysis, or summary) from research findings.
|
|
1694
1631
|
*
|
|
1695
|
-
* Reads spec.md
|
|
1696
|
-
* Writes the result to
|
|
1632
|
+
* Reads spec.md and JSONL results for context.
|
|
1633
|
+
* Writes the result to workspace and returns the filename + content.
|
|
1697
1634
|
*/
|
|
1698
1635
|
export async function generateVisualDocument(workingDir, sessionId, request, documentType, sessionBaseDir) {
|
|
1699
1636
|
initProvider();
|
|
@@ -1703,20 +1640,6 @@ export async function generateVisualDocument(workingDir, sessionId, request, doc
|
|
|
1703
1640
|
try {
|
|
1704
1641
|
const workspace = getSessionWorkspace(wsDir, sessionId);
|
|
1705
1642
|
const specContent = readSessionSpec(wsDir, sessionId) || '';
|
|
1706
|
-
const libraryFiles = listLibraryFiles(wsDir, sessionId);
|
|
1707
|
-
// Read library contents for context
|
|
1708
|
-
const libraryDir = `${workspace}/library`;
|
|
1709
|
-
const libraryContents = [];
|
|
1710
|
-
for (const file of libraryFiles.slice(0, 5)) {
|
|
1711
|
-
const filePath = `${libraryDir}/${file}`;
|
|
1712
|
-
if (existsSync(filePath)) {
|
|
1713
|
-
try {
|
|
1714
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
1715
|
-
libraryContents.push(`--- ${file} ---\n${content.substring(0, 3000)}`);
|
|
1716
|
-
}
|
|
1717
|
-
catch { /* skip */ }
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
1643
|
// Read recent JSONL results for raw data
|
|
1721
1644
|
const toolResults = getRecentToolResults(sessionId, workingDir, 20);
|
|
1722
1645
|
const toolResultsSummary = toolResults.map(tr => {
|
|
@@ -1729,8 +1652,6 @@ Document type: ${documentType}
|
|
|
1729
1652
|
Session spec:
|
|
1730
1653
|
${specContent}
|
|
1731
1654
|
|
|
1732
|
-
${libraryContents.length > 0 ? `Library files:\n${libraryContents.join('\n\n')}` : ''}
|
|
1733
|
-
|
|
1734
1655
|
Recent research data:
|
|
1735
1656
|
${toolResultsSummary}
|
|
1736
1657
|
|
|
@@ -1780,11 +1701,10 @@ Return JSON: {"fileName": "descriptive-name.md", "content": "full markdown conte
|
|
|
1780
1701
|
}
|
|
1781
1702
|
if (!parsed.fileName || !parsed.content)
|
|
1782
1703
|
return null;
|
|
1783
|
-
// Write to
|
|
1704
|
+
// Write to workspace
|
|
1784
1705
|
const safeName = parsed.fileName.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
const filePath = `${libraryPath}/${safeName}`;
|
|
1706
|
+
mkdirSync(workspace, { recursive: true });
|
|
1707
|
+
const filePath = `${workspace}/${safeName}`;
|
|
1788
1708
|
writeFileSync(filePath, parsed.content, 'utf-8');
|
|
1789
1709
|
console.log(`📊 generateVisualDocument: wrote ${safeName} (${parsed.content.length} chars)`);
|
|
1790
1710
|
return { fileName: safeName, content: parsed.content };
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { createServer } from 'http';
|
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { createPatch } from 'diff';
|
|
16
|
-
import { loadConfig, getMcpServers, getEnabledMcpServerNames, getVoiceMode, getRealtimeConfig, getDirectConfig,
|
|
16
|
+
import { loadConfig, getMcpServers, getEnabledMcpServerNames, getVoiceMode, getRealtimeConfig, getDirectConfig, listAllClaudeSessions, getMostRecentSessionId, sessionExists, getSessionSummary, getConversationHistory, ensureSessionWorkspace, getSessionWorkspace, getMcpServerStatusList, buildMcpServersForKeys, listWorkspaceArtifacts } from './config.js';
|
|
17
17
|
import { createSTT, createTTS, createRealtimeModelFromConfig, DIRECT_MODE_STT, DIRECT_MODE_TTS } from './voice-io.js';
|
|
18
18
|
import { createClaudeLLM } from './claude-llm.js';
|
|
19
19
|
import { clearPipelineFastBrainSession, prewarmBM25Index } from './pipeline-fastbrain.js';
|
|
@@ -150,14 +150,18 @@ function startApiServer(workingDir, port) {
|
|
|
150
150
|
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
151
151
|
if (req.method === 'GET' && url.pathname === '/sessions') {
|
|
152
152
|
try {
|
|
153
|
-
|
|
154
|
-
const sessions = await
|
|
153
|
+
const limit = parseInt(url.searchParams.get('limit') || '100', 10);
|
|
154
|
+
const sessions = await listAllClaudeSessions(limit);
|
|
155
155
|
const payload = {
|
|
156
156
|
sessions: sessions.map(s => ({
|
|
157
157
|
sessionId: s.sessionId,
|
|
158
|
+
projectSlug: s.projectSlug,
|
|
159
|
+
projectPath: s.projectPath,
|
|
160
|
+
cwd: s.cwd,
|
|
158
161
|
timestamp: s.timestamp.toISOString(),
|
|
159
162
|
lastMessage: s.lastMessage,
|
|
160
163
|
messageCount: s.messageCount,
|
|
164
|
+
fileSize: s.fileSize,
|
|
161
165
|
})),
|
|
162
166
|
total: sessions.length,
|
|
163
167
|
};
|
|
@@ -215,6 +219,14 @@ function startApiServer(workingDir, port) {
|
|
|
215
219
|
res.end(JSON.stringify({ roomCode: currentRoomCode }));
|
|
216
220
|
return;
|
|
217
221
|
}
|
|
222
|
+
// POST /restart — graceful process restart (process manager will restart)
|
|
223
|
+
if (req.method === 'POST' && url.pathname === '/restart') {
|
|
224
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
225
|
+
res.end(JSON.stringify({ success: true, message: 'Agent restarting...' }));
|
|
226
|
+
console.log('🔄 Restart requested via HTTP — exiting for process manager restart');
|
|
227
|
+
setTimeout(() => process.exit(0), 150);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
218
230
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
219
231
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
220
232
|
});
|
|
@@ -655,7 +667,7 @@ async function main() {
|
|
|
655
667
|
const voiceSid = currentLLM?.sessionId;
|
|
656
668
|
if (voiceSid) {
|
|
657
669
|
const chatHistory = getChatHistory(10);
|
|
658
|
-
handleResearchBatch(workingDir, voiceSid, lastTaskRequest || '', updates, activeResearch.researchLog, chatHistory,
|
|
670
|
+
handleResearchBatch(workingDir, voiceSid, lastTaskRequest || '', updates, activeResearch.researchLog, chatHistory, workingDir)
|
|
659
671
|
.then(script => {
|
|
660
672
|
if (script && activeResearch) {
|
|
661
673
|
activeResearch.voiceUpdateCount++;
|
|
@@ -776,12 +788,12 @@ async function main() {
|
|
|
776
788
|
sessionAlwaysAllowPaths = new Set();
|
|
777
789
|
// For resumed sessions, eagerly create workspace (we know the real ID)
|
|
778
790
|
if (resumeSessionId) {
|
|
779
|
-
const workspace = ensureSessionWorkspace(
|
|
791
|
+
const workspace = ensureSessionWorkspace(workingDir, resumeSessionId);
|
|
780
792
|
console.log(`📁 Session workspace (resumed): ${workspace}`);
|
|
781
793
|
}
|
|
782
794
|
// For new sessions, create workspace when SDK assigns real session ID
|
|
783
795
|
directLLM.events.once('session_id', ({ sessionId }) => {
|
|
784
|
-
const workspace = ensureSessionWorkspace(
|
|
796
|
+
const workspace = ensureSessionWorkspace(workingDir, sessionId);
|
|
785
797
|
console.log(`📁 Session workspace created: ${workspace}`);
|
|
786
798
|
// Pipeline mode: pre-warm BM25 index so first fast brain query is fast
|
|
787
799
|
if (currentVoiceMode === 'pipeline') {
|
|
@@ -812,7 +824,7 @@ async function main() {
|
|
|
812
824
|
// Detect research artifact writes (session workspace or legacy research dir)
|
|
813
825
|
if ((data.name === 'Write' || data.name === 'Edit') && data.input?.file_path) {
|
|
814
826
|
const fp = data.input.file_path;
|
|
815
|
-
if (fp.includes('.osborn/sessions/') || fp.includes('.osborn/research/')) {
|
|
827
|
+
if (fp.includes('/osb/') || fp.includes('.osborn/sessions/') || fp.includes('.osborn/research/')) {
|
|
816
828
|
sendToFrontend({
|
|
817
829
|
type: 'research_artifact_updated',
|
|
818
830
|
filePath: fp,
|
|
@@ -938,10 +950,13 @@ async function main() {
|
|
|
938
950
|
diff: diffString,
|
|
939
951
|
});
|
|
940
952
|
// Speak the descriptive request so user knows to respond
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
}
|
|
953
|
+
//do not delete!! Leave this section commented out
|
|
954
|
+
// say permission, request permission, ask for permission with session.say
|
|
955
|
+
// if (currentSession) {
|
|
956
|
+
// const ttsMessage = `${description} Say yes, no, or always.`
|
|
957
|
+
// // ;(currentSession as any).say?.(ttsMessage).catch(() => {})
|
|
958
|
+
// ;(currentSession as any).say?.(ttsMessage)
|
|
959
|
+
// }
|
|
945
960
|
});
|
|
946
961
|
// Wire up TTS say — bypass LiveKit's BufferedTokenStream, speak directly via session.say()
|
|
947
962
|
// Each text block from Claude gets spoken immediately as it arrives, no internal buffering
|
|
@@ -1046,12 +1061,12 @@ async function main() {
|
|
|
1046
1061
|
currentLLM = realtimeClaudeHandler;
|
|
1047
1062
|
// For resumed sessions, eagerly create workspace (we know the real ID)
|
|
1048
1063
|
if (resumeSessionId) {
|
|
1049
|
-
const workspace = ensureSessionWorkspace(
|
|
1064
|
+
const workspace = ensureSessionWorkspace(workingDir, resumeSessionId);
|
|
1050
1065
|
console.log(`📁 Session workspace (resumed): ${workspace}`);
|
|
1051
1066
|
}
|
|
1052
1067
|
// For new sessions, create workspace when SDK assigns real session ID
|
|
1053
1068
|
realtimeClaudeHandler.events.once('session_id', ({ sessionId }) => {
|
|
1054
|
-
const workspace = ensureSessionWorkspace(
|
|
1069
|
+
const workspace = ensureSessionWorkspace(workingDir, sessionId);
|
|
1055
1070
|
console.log(`📁 Session workspace created: ${workspace}`);
|
|
1056
1071
|
});
|
|
1057
1072
|
// Wire up MCP server changes to frontend
|
|
@@ -1074,7 +1089,7 @@ async function main() {
|
|
|
1074
1089
|
// Detect research artifact writes (session workspace or legacy research dir)
|
|
1075
1090
|
if ((data.name === 'Write' || data.name === 'Edit') && data.input?.file_path) {
|
|
1076
1091
|
const fp = data.input.file_path;
|
|
1077
|
-
if (fp.includes('.osborn/sessions/') || fp.includes('.osborn/research/')) {
|
|
1092
|
+
if (fp.includes('/osb/') || fp.includes('.osborn/sessions/') || fp.includes('.osborn/research/')) {
|
|
1078
1093
|
sendToFrontend({
|
|
1079
1094
|
type: 'research_artifact_updated',
|
|
1080
1095
|
filePath: fp,
|
|
@@ -1157,7 +1172,7 @@ async function main() {
|
|
|
1157
1172
|
// Fire-and-forget: write user question to spec.md BEFORE agent starts
|
|
1158
1173
|
const questionSid = currentLLM?.sessionId || resumeSessionId;
|
|
1159
1174
|
if (questionSid) {
|
|
1160
|
-
writeQuestionToSpec(
|
|
1175
|
+
writeQuestionToSpec(workingDir, questionSid, task).catch(err => console.error('❌ writeQuestionToSpec failed:', err));
|
|
1161
1176
|
}
|
|
1162
1177
|
// Clean up previous research UI tracking — but let the SDK query complete in background.
|
|
1163
1178
|
// The SDK has an internal queue: new query() calls enqueue behind running ones.
|
|
@@ -1229,7 +1244,7 @@ async function main() {
|
|
|
1229
1244
|
if (resultText.length > ANSWER_CHECK_THRESHOLD) {
|
|
1230
1245
|
const sid = currentLLM?.sessionId || resumeSessionId;
|
|
1231
1246
|
if (sid)
|
|
1232
|
-
checkOutputAgainstQuestions(
|
|
1247
|
+
checkOutputAgainstQuestions(workingDir, sid, resultText, 'tool_result').catch(() => { });
|
|
1233
1248
|
}
|
|
1234
1249
|
// When AskUserQuestion completes, the user's answer is a decision — track it in spec
|
|
1235
1250
|
if (data.name === 'AskUserQuestion' && data.response) {
|
|
@@ -1238,7 +1253,7 @@ async function main() {
|
|
|
1238
1253
|
const questionText = JSON.stringify(data.input?.questions || data.input || {});
|
|
1239
1254
|
const answerText = typeof data.response === 'string' ? data.response : JSON.stringify(data.response);
|
|
1240
1255
|
const specUpdate = `User answered a clarifying question during research.\nQuestion: ${questionText}\nAnswer: ${answerText}\nRecord this as a user decision in spec.md.`;
|
|
1241
|
-
askHaiku(workingDir, sid, specUpdate, undefined, undefined, undefined,
|
|
1256
|
+
askHaiku(workingDir, sid, specUpdate, undefined, undefined, undefined, workingDir).catch(err => console.error('❌ Failed to record AskUserQuestion answer in spec:', err));
|
|
1242
1257
|
console.log(`📝 AskUserQuestion answer forwarded to fast brain for spec tracking`);
|
|
1243
1258
|
}
|
|
1244
1259
|
}
|
|
@@ -1255,7 +1270,7 @@ async function main() {
|
|
|
1255
1270
|
if (text.length > ANSWER_CHECK_THRESHOLD) {
|
|
1256
1271
|
const sid = currentLLM?.sessionId || resumeSessionId;
|
|
1257
1272
|
if (sid)
|
|
1258
|
-
checkOutputAgainstQuestions(
|
|
1273
|
+
checkOutputAgainstQuestions(workingDir, sid, text, 'assistant_text').catch(() => { });
|
|
1259
1274
|
}
|
|
1260
1275
|
}
|
|
1261
1276
|
};
|
|
@@ -1368,7 +1383,7 @@ async function main() {
|
|
|
1368
1383
|
sendToFrontend({ type: 'assistant_response', text });
|
|
1369
1384
|
};
|
|
1370
1385
|
if (voiceSid) {
|
|
1371
|
-
processResearchCompletion(workingDir, voiceSid, task, result, chatHistory, completionSendToChat,
|
|
1386
|
+
processResearchCompletion(workingDir, voiceSid, task, result, chatHistory, completionSendToChat, workingDir)
|
|
1372
1387
|
.then(script => {
|
|
1373
1388
|
queueVoiceInjection(getScriptInjection(script));
|
|
1374
1389
|
})
|
|
@@ -1384,28 +1399,18 @@ async function main() {
|
|
|
1384
1399
|
// Reads FULL untruncated data from JSONL — no content buffer, no truncation
|
|
1385
1400
|
const postResearchSessionId = currentLLM?.sessionId || resumeSessionId;
|
|
1386
1401
|
if (postResearchSessionId) {
|
|
1387
|
-
updateSpecFromJSONL(workingDir, postResearchSessionId, task, researchLog,
|
|
1402
|
+
updateSpecFromJSONL(workingDir, postResearchSessionId, task, researchLog, workingDir)
|
|
1388
1403
|
.then(updateResult => {
|
|
1389
1404
|
if (!updateResult)
|
|
1390
1405
|
return;
|
|
1391
1406
|
// Notify frontend about spec.md update
|
|
1392
1407
|
if (updateResult.spec) {
|
|
1393
|
-
const specPath =
|
|
1408
|
+
const specPath = join(getSessionWorkspace(workingDir, postResearchSessionId), 'spec.md');
|
|
1394
1409
|
sendToFrontend({
|
|
1395
1410
|
type: 'research_artifact_updated',
|
|
1396
1411
|
filePath: specPath,
|
|
1397
1412
|
fileName: 'spec.md',
|
|
1398
1413
|
});
|
|
1399
|
-
// Voice model is a teleprompter — fast brain reads spec directly, no ChatCtx injection needed
|
|
1400
|
-
}
|
|
1401
|
-
// Notify frontend about each library file written by the fast brain
|
|
1402
|
-
for (const libFile of updateResult.libraryFiles) {
|
|
1403
|
-
const libPath = `${sessionBaseDir}/.osborn/sessions/${postResearchSessionId}/library/${libFile}`;
|
|
1404
|
-
sendToFrontend({
|
|
1405
|
-
type: 'research_artifact_updated',
|
|
1406
|
-
filePath: libPath,
|
|
1407
|
-
fileName: libFile,
|
|
1408
|
-
});
|
|
1409
1414
|
}
|
|
1410
1415
|
});
|
|
1411
1416
|
}
|
|
@@ -2091,16 +2096,20 @@ async function main() {
|
|
|
2091
2096
|
console.log('💓 Sending agent_ready signal...');
|
|
2092
2097
|
let readySent = false;
|
|
2093
2098
|
const provider = sessionVoiceMode === 'realtime' ? realtimeConfig.provider : 'claude';
|
|
2094
|
-
// Fetch full session list for startup session browser
|
|
2095
|
-
const allSessions = await
|
|
2099
|
+
// Fetch full session list for startup session browser (all Claude projects)
|
|
2100
|
+
const allSessions = await listAllClaudeSessions(50);
|
|
2096
2101
|
const recentSessionId = allSessions.length > 0 ? allSessions[0].sessionId : null;
|
|
2097
2102
|
const hasRecentSession = allSessions.length > 0;
|
|
2098
2103
|
// Prepare sessions for frontend (up to 50)
|
|
2099
2104
|
const sessionsForFrontend = allSessions.slice(0, 50).map(s => ({
|
|
2100
2105
|
sessionId: s.sessionId,
|
|
2106
|
+
projectSlug: s.projectSlug,
|
|
2107
|
+
projectPath: s.projectPath,
|
|
2108
|
+
cwd: s.cwd,
|
|
2101
2109
|
timestamp: s.timestamp.toISOString(),
|
|
2102
2110
|
lastMessage: s.lastMessage,
|
|
2103
2111
|
messageCount: s.messageCount,
|
|
2112
|
+
fileSize: s.fileSize,
|
|
2104
2113
|
}));
|
|
2105
2114
|
const sendReady = async () => {
|
|
2106
2115
|
if (readySent)
|
|
@@ -2157,7 +2166,7 @@ async function main() {
|
|
|
2157
2166
|
success: true,
|
|
2158
2167
|
});
|
|
2159
2168
|
// Send existing workspace artifacts to frontend (session-scoped)
|
|
2160
|
-
const preArtifacts = listWorkspaceArtifacts(
|
|
2169
|
+
const preArtifacts = listWorkspaceArtifacts(workingDir, preSelectedSessionId);
|
|
2161
2170
|
if (preArtifacts.length > 0) {
|
|
2162
2171
|
console.log(`📁 Sending ${preArtifacts.length} workspace artifacts to frontend`);
|
|
2163
2172
|
await sendToFrontend({
|
|
@@ -2177,7 +2186,7 @@ async function main() {
|
|
|
2177
2186
|
try {
|
|
2178
2187
|
if (sessionVoiceMode === 'realtime') {
|
|
2179
2188
|
const historyForScript = conversationHistory.map(e => ({ role: e.role, text: e.content }));
|
|
2180
|
-
const script = await prepareBriefingScript(
|
|
2189
|
+
const script = await prepareBriefingScript(workingDir, preSelectedSessionId, historyForScript);
|
|
2181
2190
|
await session.generateReply({ instructions: getScriptInjection(script) });
|
|
2182
2191
|
}
|
|
2183
2192
|
else {
|
|
@@ -2274,30 +2283,51 @@ async function main() {
|
|
|
2274
2283
|
}
|
|
2275
2284
|
}
|
|
2276
2285
|
else if (data.type === 'user_text' && currentSession) {
|
|
2277
|
-
|
|
2286
|
+
// Build message content — include attached files (URLs, text content)
|
|
2287
|
+
let fullContent = String(data.content || '');
|
|
2288
|
+
const files = data.files;
|
|
2289
|
+
if (files && files.length > 0) {
|
|
2290
|
+
for (const f of files) {
|
|
2291
|
+
if (f.url) {
|
|
2292
|
+
fullContent += `\n\n[${f.type === 'image' ? 'Image' : 'File'}: ${f.name}](${f.url})`;
|
|
2293
|
+
}
|
|
2294
|
+
else if (f.type === 'text' && f.content) {
|
|
2295
|
+
fullContent += `\n\n[File: ${f.name}]\n${f.content}`;
|
|
2296
|
+
}
|
|
2297
|
+
else if (f.type === 'image' && f.content) {
|
|
2298
|
+
fullContent += `\n\n[Image attached: ${f.name}]`;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
console.log(`📝 Text + ${files.length} file(s): "${fullContent.substring(0, 100)}"`);
|
|
2302
|
+
}
|
|
2303
|
+
else {
|
|
2304
|
+
console.log(`📝 Text: "${fullContent.substring(0, 100)}"`);
|
|
2305
|
+
}
|
|
2278
2306
|
// Skip interrupt for Gemini — disrupts state machine (hangs in speaking state)
|
|
2279
2307
|
if (currentProvider !== 'gemini') {
|
|
2280
2308
|
currentSession.interrupt();
|
|
2281
2309
|
}
|
|
2282
|
-
await currentSession.generateReply({ userInput:
|
|
2310
|
+
await currentSession.generateReply({ userInput: fullContent });
|
|
2283
2311
|
}
|
|
2284
2312
|
// ============================================================
|
|
2285
2313
|
// SESSION MANAGEMENT HANDLERS
|
|
2286
2314
|
// ============================================================
|
|
2287
2315
|
else if (data.type === 'list_sessions') {
|
|
2288
|
-
// List available sessions
|
|
2289
|
-
console.log('📋 Listing available sessions...');
|
|
2316
|
+
// List available sessions across all Claude projects
|
|
2317
|
+
console.log('📋 Listing available sessions (all projects)...');
|
|
2290
2318
|
try {
|
|
2291
|
-
|
|
2292
|
-
await cleanupOrphanedMetadata(workingDir);
|
|
2293
|
-
const sessions = await listSessions(workingDir);
|
|
2319
|
+
const sessions = await listAllClaudeSessions(100);
|
|
2294
2320
|
await sendToFrontend({
|
|
2295
2321
|
type: 'sessions_list',
|
|
2296
2322
|
sessions: sessions.map(s => ({
|
|
2297
2323
|
sessionId: s.sessionId,
|
|
2324
|
+
projectSlug: s.projectSlug,
|
|
2325
|
+
projectPath: s.projectPath,
|
|
2326
|
+
cwd: s.cwd,
|
|
2298
2327
|
timestamp: s.timestamp.toISOString(),
|
|
2299
2328
|
lastMessage: s.lastMessage,
|
|
2300
2329
|
messageCount: s.messageCount,
|
|
2330
|
+
fileSize: s.fileSize,
|
|
2301
2331
|
})),
|
|
2302
2332
|
count: sessions.length,
|
|
2303
2333
|
});
|
|
@@ -2327,7 +2357,7 @@ async function main() {
|
|
|
2327
2357
|
success: true,
|
|
2328
2358
|
});
|
|
2329
2359
|
// Send existing session artifacts to frontend (session-scoped)
|
|
2330
|
-
const artifacts = listWorkspaceArtifacts(
|
|
2360
|
+
const artifacts = listWorkspaceArtifacts(workingDir, sessionId);
|
|
2331
2361
|
if (artifacts.length > 0) {
|
|
2332
2362
|
console.log(`📁 Sending ${artifacts.length} session artifacts to frontend`);
|
|
2333
2363
|
await sendToFrontend({
|
|
@@ -2366,7 +2396,7 @@ async function main() {
|
|
|
2366
2396
|
success: true,
|
|
2367
2397
|
});
|
|
2368
2398
|
// Send existing session artifacts to frontend (session-scoped)
|
|
2369
|
-
const artifacts = listWorkspaceArtifacts(
|
|
2399
|
+
const artifacts = listWorkspaceArtifacts(workingDir, recentId);
|
|
2370
2400
|
if (artifacts.length > 0) {
|
|
2371
2401
|
console.log(`📁 Sending ${artifacts.length} session artifacts to frontend`);
|
|
2372
2402
|
await sendToFrontend({
|
|
@@ -2386,7 +2416,7 @@ async function main() {
|
|
|
2386
2416
|
try {
|
|
2387
2417
|
if (currentVoiceMode === 'realtime') {
|
|
2388
2418
|
const historyForScript = conversationHistory.map(e => ({ role: e.role, text: e.content }));
|
|
2389
|
-
const script = await prepareBriefingScript(
|
|
2419
|
+
const script = await prepareBriefingScript(workingDir, recentId, historyForScript);
|
|
2390
2420
|
await currentSession.generateReply({ instructions: getScriptInjection(script) });
|
|
2391
2421
|
}
|
|
2392
2422
|
else {
|
|
@@ -2431,7 +2461,7 @@ async function main() {
|
|
|
2431
2461
|
conversationHistory,
|
|
2432
2462
|
});
|
|
2433
2463
|
// Step 3.5: Send existing session artifacts to frontend (session-scoped)
|
|
2434
|
-
const switchArtifacts = listWorkspaceArtifacts(
|
|
2464
|
+
const switchArtifacts = listWorkspaceArtifacts(workingDir, sessionId);
|
|
2435
2465
|
if (switchArtifacts.length > 0) {
|
|
2436
2466
|
console.log(`📁 Sending ${switchArtifacts.length} session artifacts to frontend`);
|
|
2437
2467
|
await sendToFrontend({
|
|
@@ -2451,7 +2481,7 @@ async function main() {
|
|
|
2451
2481
|
try {
|
|
2452
2482
|
if (currentVoiceMode === 'realtime') {
|
|
2453
2483
|
const historyForScript = conversationHistory.map(e => ({ role: e.role, text: e.content }));
|
|
2454
|
-
const briefingScript = await prepareBriefingScript(
|
|
2484
|
+
const briefingScript = await prepareBriefingScript(workingDir, sessionId, historyForScript, 'switch');
|
|
2455
2485
|
queueVoiceInjection(getScriptInjection(briefingScript));
|
|
2456
2486
|
}
|
|
2457
2487
|
else {
|
|
@@ -2486,7 +2516,7 @@ async function main() {
|
|
|
2486
2516
|
else if (data.type === 'get_session_artifacts') {
|
|
2487
2517
|
const sessionId = data.sessionId;
|
|
2488
2518
|
if (sessionId) {
|
|
2489
|
-
const artifacts = listWorkspaceArtifacts(
|
|
2519
|
+
const artifacts = listWorkspaceArtifacts(workingDir, sessionId);
|
|
2490
2520
|
console.log(`📁 Sending ${artifacts.length} session artifacts for ${sessionId.substring(0, 8)}`);
|
|
2491
2521
|
await sendToFrontend({
|
|
2492
2522
|
type: 'session_artifacts',
|
|
@@ -2518,7 +2548,7 @@ async function main() {
|
|
|
2518
2548
|
}
|
|
2519
2549
|
else if (data.type === 'get_research_artifact') {
|
|
2520
2550
|
const filePath = data.filePath;
|
|
2521
|
-
if (filePath && (filePath.includes('.osborn/sessions/') || filePath.includes('.osborn/research/'))) {
|
|
2551
|
+
if (filePath && (filePath.includes('/osb/') || filePath.includes('.osborn/sessions/') || filePath.includes('.osborn/research/'))) {
|
|
2522
2552
|
try {
|
|
2523
2553
|
const fs = await import('fs');
|
|
2524
2554
|
const fileName = filePath.split('/').pop() || '';
|
|
@@ -2705,7 +2735,7 @@ async function main() {
|
|
|
2705
2735
|
success: true,
|
|
2706
2736
|
});
|
|
2707
2737
|
// Send existing session artifacts to frontend (session-scoped)
|
|
2708
|
-
const gateArtifacts = listWorkspaceArtifacts(
|
|
2738
|
+
const gateArtifacts = listWorkspaceArtifacts(workingDir, sessionId);
|
|
2709
2739
|
if (gateArtifacts.length > 0) {
|
|
2710
2740
|
console.log(`📁 Sending ${gateArtifacts.length} session artifacts to frontend`);
|
|
2711
2741
|
await sendToFrontend({
|
|
@@ -2725,7 +2755,7 @@ async function main() {
|
|
|
2725
2755
|
try {
|
|
2726
2756
|
if (currentVoiceMode === 'realtime') {
|
|
2727
2757
|
const historyForScript = conversationHistory.map(e => ({ role: e.role, text: e.content }));
|
|
2728
|
-
const briefingScript = await prepareBriefingScript(
|
|
2758
|
+
const briefingScript = await prepareBriefingScript(workingDir, sessionId, historyForScript, 'resume');
|
|
2729
2759
|
queueVoiceInjection(getScriptInjection(briefingScript));
|
|
2730
2760
|
}
|
|
2731
2761
|
else {
|