@aexol/spectral 0.6.8 → 0.6.9

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.
@@ -45,8 +45,23 @@ function renderContentToString(content) {
45
45
  }
46
46
  if (typeof first.text === "string")
47
47
  return first.text;
48
+ // For image/audio/binary content types, return a metadata placeholder
49
+ // instead of serializing the raw base64 data into the model context.
50
+ if (first.type === "image" || first.type === "audio") {
51
+ const mimeType = typeof first.mimeType === "string" ? first.mimeType : first.type;
52
+ const dataLen = typeof first.data === "string" ? first.data.length : 0;
53
+ return `[${first.type}: ${mimeType}${dataLen ? `, ${dataLen.toLocaleString()} bytes base64` : ""}]`;
54
+ }
48
55
  try {
49
- return JSON.stringify(content, null, 2);
56
+ // Strip large binary fields from serialization to prevent context overflow.
57
+ const safe = content.map((c) => {
58
+ const copy = { ...c };
59
+ if ("data" in copy && typeof copy.data === "string" && copy.data.length > 200) {
60
+ copy.data = `[${copy.data.length.toLocaleString()} bytes base64 omitted]`;
61
+ }
62
+ return copy;
63
+ });
64
+ return JSON.stringify(safe, null, 2);
50
65
  }
51
66
  catch {
52
67
  return String(content);
@@ -57,7 +57,14 @@ export function transformMcpContent(content) {
57
57
  }
58
58
  if (c.type === "resource") {
59
59
  const resourceUri = c.resource?.uri ?? "(no URI)";
60
- const resourceContent = c.resource?.text ?? (c.resource ? JSON.stringify(c.resource) : "(no content)");
60
+ // Blob resources contain base64 binary data never serialize the raw
61
+ // blob into the model context (causes context overflow and dead sessions).
62
+ const resourceContent = c.resource?.text ??
63
+ ("blob" in (c.resource ?? {}) && c.resource.blob
64
+ ? `[Binary blob: ${c.resource.mimeType ?? "application/octet-stream"}]`
65
+ : c.resource
66
+ ? JSON.stringify(c.resource)
67
+ : "(no content)");
61
68
  return truncateTextBlock({
62
69
  type: "text",
63
70
  text: `[Resource: ${resourceUri}]\n${resourceContent}`,
@@ -77,6 +84,15 @@ export function transformMcpContent(content) {
77
84
  text: `[Audio content: ${c.mimeType ?? "audio/*"}]`,
78
85
  };
79
86
  }
80
- return truncateTextBlock({ type: "text", text: JSON.stringify(c) });
87
+ // Unknown content type serialize as text but strip any potentially
88
+ // large binary fields (data, blob) to avoid context overflow.
89
+ const safe = { ...c };
90
+ if ("data" in safe && typeof safe.data === "string" && safe.data.length > 200) {
91
+ safe.data = `[${safe.data.length.toLocaleString()} bytes base64 omitted]`;
92
+ }
93
+ if ("blob" in safe && typeof safe.blob === "string" && safe.blob.length > 200) {
94
+ safe.blob = `[${safe.blob.length.toLocaleString()} bytes base64 omitted]`;
95
+ }
96
+ return truncateTextBlock({ type: "text", text: JSON.stringify(safe) });
81
97
  });
82
98
  }
@@ -699,6 +699,12 @@ export class SessionStreamManager {
699
699
  stream.loopOriginalPrompt = null;
700
700
  stream.loopGoal = null;
701
701
  stream.loopIterationCount = 0;
702
+ // Capture whether a turn is in-flight BEFORE we dispose the bridge.
703
+ // dispose() tears down pi, which can cause the in-flight prompt()
704
+ // promise to reject synchronously/microtask and emit an error event
705
+ // through handleBridgeEvent — which clears currentTurn. We must
706
+ // broadcast agent_end regardless of what dispose() does to currentTurn.
707
+ const hadTurn = stream.currentTurn != null;
702
708
  // Dispose the pi bridge immediately — this tears down pi's session and
703
709
  // unsubscribe. The bridge's own event handler is detached; no further
704
710
  // events will flow. We broadcast agent_end ourselves below.
@@ -718,8 +724,10 @@ export class SessionStreamManager {
718
724
  stream.currentMessageId = null;
719
725
  stream.lastFlushedEventCount = 0;
720
726
  // Broadcast agent_end so all subscribers close their open turn and
721
- // re-enable their composers.
722
- if (stream.currentTurn) {
727
+ // re-enable their composers. Use the pre-disposal flag — dispose()
728
+ // may have cleared currentTurn via an error event from the torn-down
729
+ // pi session.
730
+ if (hadTurn) {
723
731
  this.broadcast(stream, { type: "agent_end" });
724
732
  stream.currentTurn = null;
725
733
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,