Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ canonical five-tool memory facade.

- Captures a completed **primary** OpenCode user/assistant turn after
`session.execution.succeeded`. It excludes reasoning, tool outputs, system messages, failed
turns, interrupted turns, and subagent sessions. The stable event ID makes
repeat idle events idempotent.
turns, interrupted turns, and subagent sessions. Visible text before and
after tool calls is retained. The stable event ID makes repeat completion
events idempotent.
- Injects a bounded, clearly labeled recent chronology once per primary
session. These memories are historical data, never instructions.
- Registers `musubi_recent`, `musubi_search`, `musubi_get`,
Expand All @@ -36,9 +37,12 @@ command -v musubi-opencode-bridge
command -v musubi-opencode-mcp
```

Add the plugin **only to that seat's** `opencode.json` (the example assumes the
repository was cloned at the shown path). Then restart that seat's OpenCode
service; a background service keeps the environment from its own launch.
Add the plugin **only to that seat's project-local** `opencode.json` (the
example assumes the repository was cloned at the shown path). Do not add it to
the shared `~/.config/opencode/opencode.json`. Launch the seat with
`opencode --standalone`: the default TUI connects to a shared background
service whose environment does not contain this seat's token or identity.
Restart the seat after the project config and launcher are in place.

```json
{
Expand All @@ -62,7 +66,7 @@ The `MUSUBI_*` identity triple is all-or-nothing. The adapter refuses a
missing or mismatched identity. The JWT's unverified presence and write-scope
claims are checked locally before a remote request; the Musubi server remains
the authority for validity. Local enqueue subprocesses receive no endpoint or
token. For an OpenCode service whose `PATH` does not expose the two Python
token. For an OpenCode process whose `PATH` does not expose the two Python
commands, set `MUSUBI_OPENCODE_BRIDGE_BIN` and
`MUSUBI_OPENCODE_MCP_BIN` to their absolute paths in the launcher.

Expand Down
3 changes: 3 additions & 0 deletions plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ function bridge(request) {
export default Plugin.define({
id: "musubi-opencode",
async setup(ctx) {
if (!process.env.MUSUBI_ACTOR || !process.env.MUSUBI_PRESENCE || !process.env.MUSUBI_ZONE) {
console.error("musubi-opencode unavailable: seat identity is missing; launch OpenCode with --standalone from the seat launcher")
}
Comment on lines +31 to +33
const mcpBinary = process.env.MUSUBI_OPENCODE_MCP_BIN || "musubi-opencode-mcp"
await ctx.mcp.transform((editor) => {
editor.set("musubi", { type: "local", command: [mcpBinary] })
Expand Down
6 changes: 4 additions & 2 deletions plugin/turn.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ export function completedTurn(session, messages) {
!item.error && Number.isFinite(item.time?.completed),
)
if (!assistant) return null
const assistantText = assistant.content
?.filter((part) => part?.type === "text" && typeof part.text === "string")
const assistantText = suffix
.filter((item) => item?.type === "assistant" && !item.error)
.flatMap((item) => item.content || [])
.filter((part) => part?.type === "text" && typeof part.text === "string")
.map((part) => part.text.trim())
.filter(Boolean)
.join("\n\n")
Expand Down
7 changes: 4 additions & 3 deletions tests/turn.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ const final = {
content: [{ type: "reasoning", text: "private" }, { type: "text", text: "The code changed." }],
}

test("captures only the completed final text", () => {
expect(completedTurn(session, [user, { type: "assistant", finish: "tool-calls" }, final, { type: "idle", outcome: "succeeded" }]))
test("captures visible text across tool calls after a completed turn", () => {
const beforeTool = { type: "assistant", finish: "tool-calls", content: [{ type: "text", text: "I will check." }, { type: "tool", name: "read" }] }
expect(completedTurn(session, [user, beforeTool, final, { type: "idle", outcome: "succeeded" }]))
.toEqual({
session_id: "ses_main", user_id: "msg_user", user_text: "What changed?",
assistant_text: "The code changed.", completed_at: 3,
assistant_text: "I will check.\n\nThe code changed.", completed_at: 3,
model: "qwen-lan/qwen3.8-flash-next",
})
})
Expand Down
Loading