opencode-collaboration 0.5.2 → 0.6.1
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 +3 -1
- package/dist/index.js +26 -0
- package/dist/sanitize.d.ts +28 -0
- package/dist/sanitize.js +38 -0
- package/dist/session-runtime.js +16 -0
- package/package.json +1 -1
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.
|
|
@@ -280,6 +281,15 @@ export const PeersPlugin = async (ctx, pluginOptions) => {
|
|
|
280
281
|
if (disposing)
|
|
281
282
|
return;
|
|
282
283
|
const e = event;
|
|
284
|
+
if (e.type && e.type.includes("agent.switched")) {
|
|
285
|
+
const probe = e.properties ?? {};
|
|
286
|
+
await logger("debug", "agent switch event observed", {
|
|
287
|
+
type: e.type,
|
|
288
|
+
sessionID: typeof probe.sessionID === "string" ? probe.sessionID : undefined,
|
|
289
|
+
agent: typeof probe.agent === "string" ? probe.agent : undefined,
|
|
290
|
+
messageID: typeof probe.messageID === "string" ? probe.messageID : undefined,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
283
293
|
const changed = await runtime.handleEvent(e);
|
|
284
294
|
if (changed && !disposing)
|
|
285
295
|
await registry.heartbeat();
|
|
@@ -296,6 +306,22 @@ export const PeersPlugin = async (ctx, pluginOptions) => {
|
|
|
296
306
|
if (!disposing)
|
|
297
307
|
await registry.heartbeat();
|
|
298
308
|
},
|
|
309
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
310
|
+
if (disposing)
|
|
311
|
+
return;
|
|
312
|
+
// Clean the model-bound history before any provider sees it. opencode can
|
|
313
|
+
// leave empty text parts on tool-call turns and fully-empty assistant
|
|
314
|
+
// messages on failed turns; strict providers reject both, poisoning every
|
|
315
|
+
// later request in the session. Stripping them here is non-destructive to
|
|
316
|
+
// the stored history (parentID/tool pairing is untouched).
|
|
317
|
+
const messages = output.messages;
|
|
318
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
319
|
+
return;
|
|
320
|
+
const dropped = sanitizeMessages(messages);
|
|
321
|
+
if (dropped > 0) {
|
|
322
|
+
await logger("warn", "stripped empty assistant messages from the outgoing history", { dropped });
|
|
323
|
+
}
|
|
324
|
+
},
|
|
299
325
|
"command.execute.before": async (input, output) => {
|
|
300
326
|
if (disposing)
|
|
301
327
|
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;
|
package/dist/sanitize.js
ADDED
|
@@ -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/dist/session-runtime.js
CHANGED
|
@@ -387,6 +387,22 @@ export function SessionRuntime(opts) {
|
|
|
387
387
|
setStatus(endpoint, event.type === "session.idle" ? "idle" : normalizeStatus(properties.status));
|
|
388
388
|
return true;
|
|
389
389
|
}
|
|
390
|
+
// The user toggled the session's agent/mode (e.g. build<->plan). Peer
|
|
391
|
+
// injections use the last-known agent, so refreshing here keeps the
|
|
392
|
+
// next injected turn under the mode the user actually selected — even
|
|
393
|
+
// when the toggle happens without a follow-up user message.
|
|
394
|
+
if (event.type === "session.next.agent.switched" ||
|
|
395
|
+
event.type === "session.next.agent.switched.1") {
|
|
396
|
+
const sessionId = properties.sessionID;
|
|
397
|
+
const agent = properties.agent;
|
|
398
|
+
if (!sessionId || !agent)
|
|
399
|
+
return false;
|
|
400
|
+
const endpoint = await findSession(sessionId);
|
|
401
|
+
if (!endpoint)
|
|
402
|
+
return false;
|
|
403
|
+
endpoint.agent = agent;
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
390
406
|
return false;
|
|
391
407
|
});
|
|
392
408
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-collaboration",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|