opencode-collaboration 0.5.2 → 0.6.0

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/delivery.js CHANGED
@@ -22,7 +22,9 @@ const REPLY_DIRECTIVE = "This turn was triggered by a plain-text message from an
22
22
  "- If the message is only an acknowledgement, a thank-you, an agreement, a status update that " +
23
23
  "needs no action, or a confirmation that the task is done, do NOT reply — briefly tell your user and stop.\n" +
24
24
  "Never send acknowledgement-only, thanks, or agreement messages: they cause an endless " +
25
- "back-and-forth between agents. Once you and the peer have reached a conclusion, stop.";
25
+ "back-and-forth between agents. Once you and the peer have reached a conclusion, stop.\n" +
26
+ "When you call send_message (or any tool), always include a brief text message in the same " +
27
+ "assistant turn; never emit a bare tool call with no text.";
26
28
  const NOTICE_FOOTER = "---\n" +
27
29
  "This is an automated notification from the opencode-collaboration plugin. " +
28
30
  "Show it to the user verbatim, then stop. Do not take further action.";
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ import { Outbox } from "./outbox.js";
31
31
  import { PeerPermissions } from "./permissions.js";
32
32
  import { buildPeerTools } from "./tools/peers-tools.js";
33
33
  import { handlePeersCommand } from "./commands.js";
34
+ import { sanitizeMessages } from "./sanitize.js";
34
35
  import { consumeCommand, createLogger, errorMessage } from "./feedback.js";
35
36
  // Read the real package version at runtime so registry entries never report a
36
37
  // stale hardcoded number. Falls back to "0.0.0" if package.json is unavailable.
@@ -296,6 +297,22 @@ export const PeersPlugin = async (ctx, pluginOptions) => {
296
297
  if (!disposing)
297
298
  await registry.heartbeat();
298
299
  },
300
+ "experimental.chat.messages.transform": async (_input, output) => {
301
+ if (disposing)
302
+ return;
303
+ // Clean the model-bound history before any provider sees it. opencode can
304
+ // leave empty text parts on tool-call turns and fully-empty assistant
305
+ // messages on failed turns; strict providers reject both, poisoning every
306
+ // later request in the session. Stripping them here is non-destructive to
307
+ // the stored history (parentID/tool pairing is untouched).
308
+ const messages = output.messages;
309
+ if (!Array.isArray(messages) || messages.length === 0)
310
+ return;
311
+ const dropped = sanitizeMessages(messages);
312
+ if (dropped > 0) {
313
+ await logger("warn", "stripped empty assistant messages from the outgoing history", { dropped });
314
+ }
315
+ },
299
316
  "command.execute.before": async (input, output) => {
300
317
  if (disposing)
301
318
  return;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Sanitization for the model-bound conversation history.
3
+ *
4
+ * opencode can record tool-call turns with an empty text part (text="") and,
5
+ * when a turn fails to stream, a fully empty assistant message (0 parts).
6
+ * Strict model providers reject either ("missing input.content.text",
7
+ * "assistant must not be empty"), poisoning every later request in the
8
+ * session. We clean the outgoing history in the
9
+ * experimental.chat.messages.transform hook instead of touching storage, so
10
+ * provider-side parentID/tool pairing is never disturbed.
11
+ */
12
+ export interface SanitizeMessage {
13
+ info?: {
14
+ role?: string;
15
+ summary?: boolean;
16
+ };
17
+ parts?: Array<{
18
+ type?: string;
19
+ text?: string;
20
+ }>;
21
+ }
22
+ /**
23
+ * Mutates `messages` in place:
24
+ * - removes empty/whitespace-only text parts from every message, and
25
+ * - drops assistant messages that end up with no parts at all.
26
+ * Returns how many assistant messages were dropped.
27
+ */
28
+ export declare function sanitizeMessages<T extends SanitizeMessage>(messages: T[]): number;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Sanitization for the model-bound conversation history.
3
+ *
4
+ * opencode can record tool-call turns with an empty text part (text="") and,
5
+ * when a turn fails to stream, a fully empty assistant message (0 parts).
6
+ * Strict model providers reject either ("missing input.content.text",
7
+ * "assistant must not be empty"), poisoning every later request in the
8
+ * session. We clean the outgoing history in the
9
+ * experimental.chat.messages.transform hook instead of touching storage, so
10
+ * provider-side parentID/tool pairing is never disturbed.
11
+ */
12
+ /**
13
+ * Mutates `messages` in place:
14
+ * - removes empty/whitespace-only text parts from every message, and
15
+ * - drops assistant messages that end up with no parts at all.
16
+ * Returns how many assistant messages were dropped.
17
+ */
18
+ export function sanitizeMessages(messages) {
19
+ for (const message of messages) {
20
+ if (Array.isArray(message.parts)) {
21
+ message.parts = message.parts.filter((part) => part?.type !== "text" || (part.text ?? "").trim().length > 0);
22
+ }
23
+ }
24
+ let dropped = 0;
25
+ for (let index = messages.length - 1; index >= 0; index--) {
26
+ const message = messages[index];
27
+ const role = message.info?.role;
28
+ // Never drop compaction summary messages (info.summary === true): the model
29
+ // may depend on them for context that predates the compaction.
30
+ if (role === "assistant" &&
31
+ message.info?.summary !== true &&
32
+ (!message.parts || message.parts.length === 0)) {
33
+ messages.splice(index, 1);
34
+ dropped++;
35
+ }
36
+ }
37
+ return dropped;
38
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-collaboration",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Cross-session messaging for opencode — let independent sessions discover and text each other, modeled after Claude Code's cross-session messaging",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",