osborn 0.8.14 → 0.8.15

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.
Files changed (2) hide show
  1. package/dist/index.js +53 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -818,7 +818,13 @@ async function main() {
818
818
  proactivePromptHistory = [];
819
819
  }
820
820
  // Helper to send data to frontend (with size limit handling)
821
- const MAX_MESSAGE_SIZE = 60000;
821
+ //
822
+ // WebRTC SCTP data channel max message size is ~256KB. Sending larger
823
+ // payloads corrupts the publisher transport, killing ALL subsequent sends.
824
+ // We enforce a soft limit (truncate text/content fields) and a hard limit
825
+ // (drop the message entirely with a warning) to prevent this.
826
+ const MAX_MESSAGE_SIZE = 60000; // soft limit — truncate text/content fields
827
+ const HARD_MAX_MESSAGE_SIZE = 200000; // hard limit — drop if still too large after truncation
822
828
  async function sendToFrontend(data) {
823
829
  if (!localParticipant) {
824
830
  console.log('⚠️ sendToFrontend: no localParticipant!');
@@ -827,18 +833,36 @@ async function main() {
827
833
  try {
828
834
  const encoder = new TextEncoder();
829
835
  let jsonData = JSON.stringify(data);
830
- // If message is too large, truncate the text content
836
+ // If message is too large, truncate the text or content field
831
837
  if (jsonData.length > MAX_MESSAGE_SIZE) {
832
838
  const truncatedData = { ...data };
839
+ // Try truncating .text first (assistant_response, claude_output, etc.)
833
840
  if (truncatedData.text && typeof truncatedData.text === 'string') {
834
841
  const overhead = JSON.stringify({ ...truncatedData, text: '' }).length;
835
842
  const maxTextLength = MAX_MESSAGE_SIZE - overhead - 100;
836
843
  truncatedData.text = truncatedData.text.substring(0, maxTextLength) + '\n\n[Message truncated due to size limit]';
837
844
  jsonData = JSON.stringify(truncatedData);
838
- console.log(`⚠️ Message truncated from ${data.text?.length} to ${truncatedData.text.length} chars`);
845
+ console.log(`⚠️ Message truncated .text from ${data.text?.length} to ${truncatedData.text.length} chars`);
846
+ }
847
+ // Also try truncating .content (research_artifact_content, plan_file_content)
848
+ if (jsonData.length > MAX_MESSAGE_SIZE && truncatedData.content && typeof truncatedData.content === 'string') {
849
+ const overhead = JSON.stringify({ ...truncatedData, content: '' }).length;
850
+ const maxContentLength = MAX_MESSAGE_SIZE - overhead - 100;
851
+ truncatedData.content = truncatedData.content.substring(0, maxContentLength) + '\n\n[Content truncated due to size limit]';
852
+ truncatedData.truncated = true;
853
+ truncatedData.originalSize = Buffer.byteLength(data.content, 'utf-8');
854
+ jsonData = JSON.stringify(truncatedData);
855
+ console.log(`⚠️ Message truncated .content from ${data.content?.length} to ${truncatedData.content.length} chars`);
839
856
  }
840
857
  }
858
+ // Hard cap — if still too large after truncation, drop entirely.
859
+ // This prevents a 480KB base64 image or similar from killing the
860
+ // WebRTC publisher transport (which is unrecoverable without reconnect).
841
861
  const payload = encoder.encode(jsonData);
862
+ if (payload.length > HARD_MAX_MESSAGE_SIZE) {
863
+ console.error(`❌ sendToFrontend: dropping message (${(payload.length / 1024).toFixed(0)}KB > ${(HARD_MAX_MESSAGE_SIZE / 1024).toFixed(0)}KB hard limit) — type: ${data.type}`);
864
+ return;
865
+ }
842
866
  await localParticipant.publishData(payload, {
843
867
  reliable: true,
844
868
  topic: 'osborn-updates',
@@ -2677,13 +2701,36 @@ async function main() {
2677
2701
  const fileName = filePath.split('/').pop() || '';
2678
2702
  const ext = fileName.split('.').pop()?.toLowerCase() || '';
2679
2703
  const isImage = ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext);
2704
+ // WebRTC SCTP data channel max message size is ~256KB. Sending
2705
+ // larger payloads corrupts the publisher transport, killing ALL
2706
+ // subsequent sends (publishData, streamBytes, publishTranscription)
2707
+ // with "could not establish publisher connection: timeout". This
2708
+ // was the root cause of the career-ops session bug: a 480KB
2709
+ // evaluation report blew through the limit on resume.
2710
+ const MAX_DATA_CHANNEL_BYTES = 200_000; // 200KB — safe margin under 256KB SCTP limit
2680
2711
  if (isImage) {
2681
- const base64 = fs.readFileSync(filePath, 'base64');
2682
- await sendToFrontend({ type: 'research_artifact_content', filePath, content: base64, fileName, isImage: true, mimeType: `image/${ext}` });
2712
+ const stats = fs.statSync(filePath);
2713
+ const base64Size = Math.ceil(stats.size * 4 / 3); // base64 inflates ~33%
2714
+ if (base64Size > MAX_DATA_CHANNEL_BYTES) {
2715
+ console.log(`⚠️ Artifact too large for data channel: ${fileName} (${(base64Size / 1024).toFixed(0)}KB base64) — sending truncation notice`);
2716
+ await sendToFrontend({ type: 'research_artifact_content', filePath, content: '', fileName, isImage: false, truncated: true, originalSize: stats.size });
2717
+ }
2718
+ else {
2719
+ const base64 = fs.readFileSync(filePath, 'base64');
2720
+ await sendToFrontend({ type: 'research_artifact_content', filePath, content: base64, fileName, isImage: true, mimeType: `image/${ext}` });
2721
+ }
2683
2722
  }
2684
2723
  else {
2685
2724
  const content = fs.readFileSync(filePath, 'utf-8');
2686
- await sendToFrontend({ type: 'research_artifact_content', filePath, content, fileName, isImage: false });
2725
+ if (Buffer.byteLength(content, 'utf-8') > MAX_DATA_CHANNEL_BYTES) {
2726
+ // Send a truncated preview + metadata so the frontend knows the file exists
2727
+ const truncated = content.substring(0, 50_000); // ~50KB text preview
2728
+ console.log(`⚠️ Artifact too large for data channel: ${fileName} (${(Buffer.byteLength(content, 'utf-8') / 1024).toFixed(0)}KB) — sending truncated preview`);
2729
+ await sendToFrontend({ type: 'research_artifact_content', filePath, content: truncated, fileName, isImage: false, truncated: true, originalSize: Buffer.byteLength(content, 'utf-8') });
2730
+ }
2731
+ else {
2732
+ await sendToFrontend({ type: 'research_artifact_content', filePath, content, fileName, isImage: false });
2733
+ }
2687
2734
  }
2688
2735
  }
2689
2736
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.8.14",
3
+ "version": "0.8.15",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {