osborn 0.9.6 → 0.9.12
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.d.ts +5 -0
- package/dist/claude-llm.js +100 -39
- package/dist/index.js +40 -0
- package/dist/prompts/compact-learnings-instruction.md +53 -10
- package/package.json +1 -1
package/dist/claude-llm.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface ClaudeLLMOptions {
|
|
|
21
21
|
model?: string;
|
|
22
22
|
voiceMode?: 'direct' | 'realtime';
|
|
23
23
|
skipTTSQueue?: boolean;
|
|
24
|
+
onCompactionEvent?: (event: {
|
|
25
|
+
type: 'compaction_started' | 'compaction_complete';
|
|
26
|
+
trigger?: string;
|
|
27
|
+
skillsWritten?: number;
|
|
28
|
+
}) => void;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* Claude LLM - Wraps Claude Agent SDK for LiveKit
|
package/dist/claude-llm.js
CHANGED
|
@@ -547,6 +547,8 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
547
547
|
* @param callbacks - Event callbacks for the background consumer
|
|
548
548
|
*/
|
|
549
549
|
pushMessage(userText, sdkOptions, callbacks) {
|
|
550
|
+
// Lower compaction threshold to 65% so PreCompact fires earlier and context is preserved
|
|
551
|
+
process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = '75';
|
|
550
552
|
const userMessage = {
|
|
551
553
|
type: 'user',
|
|
552
554
|
message: { role: 'user', content: [{ type: 'text', text: userText }] },
|
|
@@ -912,30 +914,19 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
912
914
|
}
|
|
913
915
|
}]
|
|
914
916
|
}],
|
|
915
|
-
// ── PreCompact: inject
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
// existing learned skills so Claude can merge/update rather than start fresh.
|
|
917
|
+
// ── PreCompact: inject extraction instruction only ──
|
|
918
|
+
// Fires before the SDK compresses the conversation. Injects the compact-learnings
|
|
919
|
+
// instruction so the SDK compaction summary includes the four structured sections.
|
|
920
|
+
// Skill extraction happens in PostCompact, which reads input.compact_summary directly.
|
|
920
921
|
PreCompact: [{
|
|
921
922
|
matcher: '.*',
|
|
922
923
|
hooks: [async (input) => {
|
|
923
924
|
try {
|
|
925
|
+
this.#opts.onCompactionEvent?.({ type: 'compaction_started', trigger: input?.trigger });
|
|
924
926
|
const instructionPath = join(__claudeLlmDir, 'prompts', 'compact-learnings-instruction.md');
|
|
925
|
-
const instruction = readFileSync(instructionPath, 'utf-8');
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const skillPath = join(skillDir, '.claude', 'skills', 'learned-behaviors', 'SKILL.md');
|
|
929
|
-
let existingSkills = '';
|
|
930
|
-
try {
|
|
931
|
-
existingSkills = readFileSync(skillPath, 'utf-8');
|
|
932
|
-
}
|
|
933
|
-
catch { }
|
|
934
|
-
const fullInstruction = existingSkills
|
|
935
|
-
? `${instruction}\n\nEXISTING LEARNED SKILLS (update/merge — remove outdated, add new, strengthen confirmed):\n${existingSkills}`
|
|
936
|
-
: instruction;
|
|
937
|
-
console.log(`🧠 PreCompact: injected learnings instruction (${fullInstruction.length} chars, trigger=${input?.trigger || 'unknown'})`);
|
|
938
|
-
return { systemMessage: fullInstruction };
|
|
927
|
+
const instruction = existsSync(instructionPath) ? readFileSync(instructionPath, 'utf-8') : '';
|
|
928
|
+
console.log(`🧠 PreCompact: injecting instruction (${instruction.length} chars, trigger=${input?.trigger || 'unknown'})`);
|
|
929
|
+
return { systemMessage: instruction };
|
|
939
930
|
}
|
|
940
931
|
catch (err) {
|
|
941
932
|
console.error('⚠️ PreCompact hook error:', err instanceof Error ? err.message : err);
|
|
@@ -943,34 +934,104 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
943
934
|
}
|
|
944
935
|
}]
|
|
945
936
|
}],
|
|
946
|
-
// ── PostCompact:
|
|
937
|
+
// ── PostCompact: parse compact_summary from stdin payload and persist skill sections ──
|
|
938
|
+
// input.compact_summary is the full summary text the SDK just wrote.
|
|
939
|
+
// We read the four sections directly from it — no JSONL file access needed.
|
|
947
940
|
PostCompact: [{
|
|
948
941
|
matcher: '.*',
|
|
949
942
|
hooks: [async (input) => {
|
|
950
943
|
try {
|
|
951
944
|
const summary = input?.compact_summary || '';
|
|
952
|
-
const
|
|
953
|
-
const idx = summary.indexOf(marker);
|
|
954
|
-
if (idx === -1) {
|
|
955
|
-
console.log('🧠 PostCompact: no BEHAVIORAL_LEARNINGS section found in summary — skipping');
|
|
956
|
-
return {};
|
|
957
|
-
}
|
|
958
|
-
const learnings = summary.substring(idx + marker.length).trim();
|
|
959
|
-
if (learnings.length < 30) {
|
|
960
|
-
console.log('🧠 PostCompact: BEHAVIORAL_LEARNINGS section too short — skipping');
|
|
961
|
-
return {};
|
|
962
|
-
}
|
|
963
|
-
// Write the skill file
|
|
945
|
+
const { mkdirSync, writeFileSync: writeSyncFs, readFileSync: readSyncFs, existsSync: existsSyncFs } = await import('node:fs');
|
|
964
946
|
const skillDir = this.#opts.sessionBaseDir || this.#opts.workingDirectory || process.cwd();
|
|
965
|
-
const skillFolder = join(skillDir, '.claude', 'skills', 'learned-behaviors');
|
|
966
|
-
const skillPath = join(skillFolder, 'SKILL.md');
|
|
967
|
-
const { mkdirSync, writeFileSync: writeSyncFs } = await import('fs');
|
|
968
|
-
mkdirSync(skillFolder, { recursive: true });
|
|
969
947
|
const today = new Date().toISOString().split('T')[0];
|
|
970
948
|
const sessionId = this.#sessionId || 'unknown';
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
949
|
+
let skillsWritten = 0;
|
|
950
|
+
// Helper: extract text between a marker and the next === marker (or end of string)
|
|
951
|
+
const extractSection = (marker) => {
|
|
952
|
+
const idx = summary.indexOf(marker);
|
|
953
|
+
if (idx === -1)
|
|
954
|
+
return '';
|
|
955
|
+
const start = idx + marker.length;
|
|
956
|
+
const nextMarker = summary.indexOf('=== ', start);
|
|
957
|
+
return (nextMarker === -1 ? summary.substring(start) : summary.substring(start, nextMarker)).trim();
|
|
958
|
+
};
|
|
959
|
+
// ── Section 1: HANDOFF_STATE — stays in compact summary only, not written to disk ──
|
|
960
|
+
const handoff = extractSection('=== HANDOFF_STATE ===');
|
|
961
|
+
if (handoff) {
|
|
962
|
+
console.log(`🧠 PostCompact: HANDOFF_STATE present (${handoff.length} chars) — not written to disk`);
|
|
963
|
+
}
|
|
964
|
+
// ── Section 2: DECISIONS — append project-scoped decisions ──
|
|
965
|
+
try {
|
|
966
|
+
const decisions = extractSection('=== DECISIONS ===');
|
|
967
|
+
if (decisions) {
|
|
968
|
+
const projectLines = decisions
|
|
969
|
+
.split('\n')
|
|
970
|
+
.filter(l => /DECISION:.*SCOPE:\s*project/i.test(l));
|
|
971
|
+
if (projectLines.length) {
|
|
972
|
+
const decFolder = join(skillDir, '.claude', 'skills', 'decisions');
|
|
973
|
+
const decPath = join(decFolder, 'SKILL.md');
|
|
974
|
+
mkdirSync(decFolder, { recursive: true });
|
|
975
|
+
const existing = existsSyncFs(decPath) ? readSyncFs(decPath, 'utf-8') : '';
|
|
976
|
+
const header = existing ? '' : `# Project Decisions\n\nAuto-extracted from compact summaries.\n\n`;
|
|
977
|
+
const entry = `\n## ${today} (session ${sessionId.substring(0, 8)})\n${projectLines.join('\n')}\n`;
|
|
978
|
+
writeSyncFs(decPath, header + existing + entry, 'utf-8');
|
|
979
|
+
console.log(`🧠 PostCompact: appended ${projectLines.length} decision(s) to ${decPath}`);
|
|
980
|
+
skillsWritten++;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
catch (decErr) {
|
|
985
|
+
console.error('⚠️ PostCompact: DECISIONS write failed:', decErr instanceof Error ? decErr.message : decErr);
|
|
986
|
+
}
|
|
987
|
+
// ── Section 3: SKILL_CANDIDATES — parse individual blocks and write each ──
|
|
988
|
+
try {
|
|
989
|
+
const skillsSection = extractSection('=== SKILL_CANDIDATES ===');
|
|
990
|
+
if (skillsSection) {
|
|
991
|
+
const skillBlockRe = /---\s*SKILL:\s*([^\n-]+?)\s*---\n([\s\S]*?)---\s*END SKILL\s*---/g;
|
|
992
|
+
const nameRe = /^[a-z][a-z0-9-]{1,39}$/;
|
|
993
|
+
let match;
|
|
994
|
+
while ((match = skillBlockRe.exec(skillsSection)) !== null) {
|
|
995
|
+
const name = match[1].trim();
|
|
996
|
+
const body = match[2].trim();
|
|
997
|
+
if (!nameRe.test(name)) {
|
|
998
|
+
console.warn(`⚠️ PostCompact: skipping skill with invalid name "${name}" (must match /^[a-z][a-z0-9-]{1,39}$/)`);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const skillFolder = join(skillDir, '.claude', 'skills', name);
|
|
1002
|
+
const skillPath = join(skillFolder, 'SKILL.md');
|
|
1003
|
+
mkdirSync(skillFolder, { recursive: true });
|
|
1004
|
+
const header = `# ${name}\nAuto-extracted: ${today} | Session: ${sessionId.substring(0, 8)}\n\n`;
|
|
1005
|
+
writeSyncFs(skillPath, header + body + '\n', 'utf-8');
|
|
1006
|
+
console.log(`🧠 PostCompact: wrote skill '${name}' to ${skillPath}`);
|
|
1007
|
+
skillsWritten++;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch (skillErr) {
|
|
1012
|
+
console.error('⚠️ PostCompact: SKILL_CANDIDATES write failed:', skillErr instanceof Error ? skillErr.message : skillErr);
|
|
1013
|
+
}
|
|
1014
|
+
// ── Section 4: BEHAVIORAL_LEARNINGS — write to learned-behaviors/SKILL.md ──
|
|
1015
|
+
try {
|
|
1016
|
+
const learnings = extractSection('=== BEHAVIORAL_LEARNINGS ===');
|
|
1017
|
+
if (learnings.length >= 30) {
|
|
1018
|
+
const skillFolder = join(skillDir, '.claude', 'skills', 'learned-behaviors');
|
|
1019
|
+
const skillPath = join(skillFolder, 'SKILL.md');
|
|
1020
|
+
mkdirSync(skillFolder, { recursive: true });
|
|
1021
|
+
const header = `# Learned Behaviors\n\nAuto-extracted from voice sessions via PostCompact.\nLast updated: ${today} | Session: ${sessionId.substring(0, 8)}...\n\n`;
|
|
1022
|
+
writeSyncFs(skillPath, header + learnings + '\n', 'utf-8');
|
|
1023
|
+
console.log(`🧠 PostCompact: wrote learned behaviors to ${skillPath} (${learnings.length} chars)`);
|
|
1024
|
+
skillsWritten++;
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
console.log('🧠 PostCompact: no BEHAVIORAL_LEARNINGS section found or too short — skipping');
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
catch (blErr) {
|
|
1031
|
+
console.error('⚠️ PostCompact: BEHAVIORAL_LEARNINGS write failed:', blErr instanceof Error ? blErr.message : blErr);
|
|
1032
|
+
}
|
|
1033
|
+
this.#opts.onCompactionEvent?.({ type: 'compaction_complete', skillsWritten });
|
|
1034
|
+
console.log(`🧠 PostCompact: complete — ${skillsWritten} skill file(s) written`);
|
|
974
1035
|
}
|
|
975
1036
|
catch (err) {
|
|
976
1037
|
console.error('⚠️ PostCompact hook error:', err instanceof Error ? err.message : err);
|
package/dist/index.js
CHANGED
|
@@ -622,6 +622,34 @@ function startApiServer(workingDir, port) {
|
|
|
622
622
|
console.log(`🌐 API server listening on http://${host}:${port}`);
|
|
623
623
|
console.log(` Sessions: http://${host}:${port}/sessions`);
|
|
624
624
|
});
|
|
625
|
+
// Stale upload-chunk cleanup: remove osborn-upload-* dirs older than 30 minutes
|
|
626
|
+
const cleanStaleUploadDirs = () => {
|
|
627
|
+
const tmp = tmpdir();
|
|
628
|
+
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
629
|
+
try {
|
|
630
|
+
const entries = readdirSync(tmp);
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
if (!entry.startsWith('osborn-upload-'))
|
|
633
|
+
continue;
|
|
634
|
+
const full = `${tmp}/${entry}`;
|
|
635
|
+
try {
|
|
636
|
+
const st = statSync(full);
|
|
637
|
+
if (st.isDirectory() && st.mtimeMs < cutoff) {
|
|
638
|
+
rmSync(full, { recursive: true, force: true });
|
|
639
|
+
console.log(`🧹 Removed stale upload dir: ${full}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
// ignore per-entry errors
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// ignore if /tmp is unreadable
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
cleanStaleUploadDirs();
|
|
652
|
+
setInterval(cleanStaleUploadDirs, 10 * 60 * 1000);
|
|
625
653
|
server.on('error', (err) => {
|
|
626
654
|
if (err.code === 'EADDRINUSE') {
|
|
627
655
|
console.warn(`⚠️ API port ${port} in use, trying ${port + 1}...`);
|
|
@@ -1260,6 +1288,12 @@ async function main() {
|
|
|
1260
1288
|
resumeSessionId,
|
|
1261
1289
|
voiceMode: 'direct',
|
|
1262
1290
|
skipTTSQueue: true,
|
|
1291
|
+
onCompactionEvent: (event) => {
|
|
1292
|
+
try {
|
|
1293
|
+
sendToFrontend({ type: event.type, trigger: event.trigger, skillsWritten: event.skillsWritten });
|
|
1294
|
+
}
|
|
1295
|
+
catch { /* non-fatal */ }
|
|
1296
|
+
},
|
|
1263
1297
|
});
|
|
1264
1298
|
currentLLM = directLLM;
|
|
1265
1299
|
// Reset the session always-allow list for each new direct session
|
|
@@ -1587,6 +1621,12 @@ async function main() {
|
|
|
1587
1621
|
sessionBaseDir,
|
|
1588
1622
|
mcpServers,
|
|
1589
1623
|
resumeSessionId,
|
|
1624
|
+
onCompactionEvent: (event) => {
|
|
1625
|
+
try {
|
|
1626
|
+
sendToFrontend({ type: event.type, trigger: event.trigger, skillsWritten: event.skillsWritten });
|
|
1627
|
+
}
|
|
1628
|
+
catch { /* non-fatal */ }
|
|
1629
|
+
},
|
|
1590
1630
|
});
|
|
1591
1631
|
currentLLM = realtimeClaudeHandler;
|
|
1592
1632
|
// For resumed sessions, eagerly create workspace (we know the real ID)
|
|
@@ -1,14 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
# Pre-Compaction Instructions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Before Claude Code compacts this conversation, you MUST preserve critical context by including the following four sections at the very END of your compact summary. Be selective and specific — vague summaries are useless.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
1. USER CORRECTIONS — things the user explicitly told you to stop/start doing (e.g., "stop patching scripts", "use step-by-step not autonomous", "don't name sub-agents")
|
|
7
|
-
2. USER PREFERENCES — recurring patterns in how the user works (e.g., "prefers yahoo email for new accounts", "wants visible browser not headless", "expects score-based salary calculation")
|
|
8
|
-
3. DOMAIN KNOWLEDGE — specific technical facts verified during this session (e.g., "Workday sign-in is inline switch not modal", "CareSource hibernation is 30s confirmed")
|
|
9
|
-
4. EFFECTIVE PATTERNS — approaches that worked and the user confirmed (e.g., "co-direction questions during steerable work", "grounding with 2 reads before speaking")
|
|
10
|
-
5. ANTI-PATTERNS — approaches that failed or frustrated the user (e.g., "reporting exit code 0 as success without verification", "multiple browser windows from competing automation approaches")
|
|
5
|
+
---
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
## Section 1: HANDOFF_STATE
|
|
8
|
+
Format: === HANDOFF_STATE ===
|
|
9
|
+
Include:
|
|
10
|
+
- **Current goal**: What are we building/fixing and WHY (the big-picture reason, not just the immediate task)
|
|
11
|
+
- **Progress**: What is done, what is in-progress, what is the very next step
|
|
12
|
+
- **Active facts**: Environment details mentioned in this session (API keys referenced, service URLs, version numbers confirmed, file paths that matter)
|
|
13
|
+
- **Test results**: What was tried, what it showed, what it ruled out
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Keep this under 800 characters. This stays in the compact summary — it is NOT written to disk.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Section 2: DECISIONS
|
|
20
|
+
Format: === DECISIONS ===
|
|
21
|
+
List each architectural or project decision made in this session, one per line:
|
|
22
|
+
- DECISION: <choice made> | RATIONALE: <why> | SCOPE: project
|
|
23
|
+
|
|
24
|
+
Only include decisions that would matter in a future session on the same project. Skip trivial choices.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Section 3: SKILL_CANDIDATES
|
|
29
|
+
Format: === SKILL_CANDIDATES ===
|
|
30
|
+
For each reusable how-to procedure confirmed working in this session, emit:
|
|
31
|
+
|
|
32
|
+
--- SKILL: <kebab-case-name> ---
|
|
33
|
+
WHEN: <one line: when this skill applies>
|
|
34
|
+
STEPS:
|
|
35
|
+
1. ...
|
|
36
|
+
2. ...
|
|
37
|
+
VERIFIED: <exact command or observation that confirmed it works>
|
|
38
|
+
--- END SKILL ---
|
|
39
|
+
|
|
40
|
+
A skill is worth extracting only if: (1) it was confirmed working in this session AND (2) it would apply to future sessions on different tasks. Do NOT re-emit skills already shown in the EXISTING SKILLS section unless they need substantive updates.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Section 4: BEHAVIORAL_LEARNINGS
|
|
45
|
+
Format: === BEHAVIORAL_LEARNINGS ===
|
|
46
|
+
Capture user corrections, preferences, and anti-patterns in these subsections:
|
|
47
|
+
USER CORRECTIONS:
|
|
48
|
+
USER PREFERENCES:
|
|
49
|
+
DOMAIN KNOWLEDGE:
|
|
50
|
+
EFFECTIVE PATTERNS:
|
|
51
|
+
ANTI-PATTERNS:
|
|
52
|
+
|
|
53
|
+
If the EXISTING LEARNED SKILLS section is shown below, merge — update outdated items, add new ones, keep confirmed ones.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
IMPORTANT: All four sections MUST appear at the end of the compact summary even if some are empty. Empty sections should have a single line: (none this session)
|