osborn 0.9.138 → 0.9.139

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.
@@ -25,7 +25,7 @@ function buildSessionTail(sessionId, workingDir) {
25
25
  return '';
26
26
  if (!sessionId)
27
27
  return '';
28
- const maxLines = parseInt(process.env.OSBORN_SESSION_TAIL_COUNT || '1000', 10);
28
+ const maxLines = parseInt(process.env.OSBORN_SESSION_TAIL_COUNT || '1500', 10);
29
29
  try {
30
30
  const indexPath = getIndexPath(sessionId, workingDir);
31
31
  if (!indexPath)
@@ -63,14 +63,30 @@ export class PipelineDirectLLM extends llm.LLM {
63
63
  #turnAbort = null;
64
64
  #indexWatcher = null;
65
65
  #indexBuilding = false;
66
+ // Set to true when PostCompact fires — cleared after injecting the tail once
67
+ // on the next real user turn (seam-only injection, never per-turn).
68
+ #postCompactionTailPending = false;
66
69
  // True while the in-flight turn is a [MEETING —] chunk — the tts_say gate in
67
70
  // index.ts reads this to skip browser session.say() (meeting audio goes via
68
71
  // /canvas, not the laptop speakers → no same-room feedback). Set per chat().
69
72
  suppressMeetingTTS = false;
70
73
  constructor(opts) {
71
74
  super();
72
- this.#claudeLLM = new ClaudeLLM(opts);
73
- this.#opts = opts;
75
+ // Chain the onCompactionEvent callback so we can set #postCompactionTailPending
76
+ // when compaction completes, without breaking the existing UI-notification path.
77
+ const upstreamCompactionEvent = opts.onCompactionEvent;
78
+ const chainedOpts = {
79
+ ...opts,
80
+ onCompactionEvent: (event) => {
81
+ if (event.type === 'compaction_complete') {
82
+ this.#postCompactionTailPending = true;
83
+ console.log('[SEAM] compaction complete → will inject session_tail on next turn');
84
+ }
85
+ upstreamCompactionEvent?.(event);
86
+ },
87
+ };
88
+ this.#claudeLLM = new ClaudeLLM(chainedOpts);
89
+ this.#opts = chainedOpts;
74
90
  }
75
91
  /** Stop the index watcher (call on disconnect/session switch) */
76
92
  stopIndexWatcher() {
@@ -133,16 +149,25 @@ export class PipelineDirectLLM extends llm.LLM {
133
149
  }
134
150
  }
135
151
  console.log(`📥 [pipeline] chat() call #${callN} (${userText.length} chars): "${userText}"`);
136
- // ── SESSION TAIL injection ────────────────────────────────────────────────
137
- // Prepend a compact recent-conversation block so the orchestrator never
138
- // loses thread across compaction. Runs synchronously (readFileSync) but the
139
- // index file is ≤1MB and kept hot in the OS page cache — negligible latency.
140
- // Skipped for meeting chunks (they are not real user turns).
141
- const sessionTailBlock = (!userText.startsWith('[MEETING'))
142
- ? buildSessionTail(this.#claudeLLM.sessionId, this.#opts.workingDirectory || process.cwd())
143
- : '';
144
- if (sessionTailBlock) {
145
- console.log(`🧵 [pipeline] Session tail: ${sessionTailBlock.split('\n').length - 2} lines ready`);
152
+ // ── SESSION TAIL injection (seam-only) ───────────────────────────────────
153
+ // Inject the tail ONCE on the first real user turn after a compaction
154
+ // completes. Never injected on every turn (avoids catastrophic accumulation
155
+ // in the SDK persistent subprocess history). Meeting chunks are not real
156
+ // user turns and never get the tail.
157
+ let sessionTailBlock = '';
158
+ if (this.#postCompactionTailPending && !userText.startsWith('[MEETING') && userText.trim()) {
159
+ sessionTailBlock = buildSessionTail(this.#claudeLLM.sessionId, this.#opts.workingDirectory || process.cwd());
160
+ if (sessionTailBlock) {
161
+ const tailLines = sessionTailBlock.split('\n').length - 2;
162
+ const tailChars = sessionTailBlock.length;
163
+ console.log(`[SEAM] injecting post-compaction session_tail: ${tailLines} lines, ~${tailChars} chars`);
164
+ }
165
+ else {
166
+ console.log('[SEAM] post-compaction turn but session_tail empty/disabled');
167
+ }
168
+ // Always clear the flag after the first real turn, whether or not the
169
+ // index file had content (avoids re-trying on an empty index).
170
+ this.#postCompactionTailPending = false;
146
171
  }
147
172
  // Always check the pending playback context — it can carry two independent
148
173
  // signals: (a) an actual interruption (spokenText + recentMessages) when the
@@ -227,7 +252,7 @@ export class PipelineDirectLLM extends llm.LLM {
227
252
  // when text is non-empty.
228
253
  enrichedMessage = userText;
229
254
  }
230
- // Modify the last user message in chatCtx — prepend session tail if available
255
+ // Modify the last user message in chatCtx
231
256
  const finalMessage = sessionTailBlock
232
257
  ? sessionTailBlock + '\n\n' + enrichedMessage
233
258
  : enrichedMessage;
@@ -240,7 +265,7 @@ export class PipelineDirectLLM extends llm.LLM {
240
265
  }
241
266
  }
242
267
  else if (sessionTailBlock && userText.trim()) {
243
- // No interrupt context — inject session tail directly into the user message
268
+ // No interrupt context — inject post-compaction session tail into the user message
244
269
  for (let i = chatCtx.items.length - 1; i >= 0; i--) {
245
270
  const item = chatCtx.items[i];
246
271
  if (item.type === 'message' && item.role === 'user') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.138",
3
+ "version": "0.9.139",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {