From fe73d2eeb52f12580449212c39c4d74ef340e0ba Mon Sep 17 00:00:00 2001 From: Rui Rei Date: Fri, 5 Jun 2026 14:38:51 +0100 Subject: [PATCH 1/2] fix: use bare peerName for the user peer to match sibling plugins The user peer was derived as `user:${peerName}` (normalised to `user-`), whereas the claude-honcho and hermes-honcho plugins use the bare `peerName`. In a shared workspace this split the user's memory into a separate `user-` collection, so OpenCode never saw or contributed to what the other tools learned. Drop the prefix so the user peer matches the sibling plugins. --- src/index.ts | 6 +++++- tests/context-injection.test.js | 2 +- tests/peer-topology.test.js | 8 ++++---- tests/user-peer-id.test.js | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 tests/user-peer-id.test.js diff --git a/src/index.ts b/src/index.ts index f19e669..23a41d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -616,6 +616,9 @@ const writeSettings = async ( const currentUserName = () => "user" +const deriveUserPeerId = (settings: Pick) => + normalizeId(settings.peerName || currentUserName()) + const rootApiKey = (raw: Record) => { const legacyApiKey = typeof raw[LEGACY_API_KEY_FIELD] === "string" ? expandEnv(raw[LEGACY_API_KEY_FIELD] as string) : "" return legacyApiKey @@ -754,7 +757,7 @@ const deriveRuntimeHandle = async ( const sessionId = extractSessionId(input) const repoName = path.basename(rootDir) const workspaceId = normalizeId(settings.workspace || "opencode") - const userPeerId = normalizeId(`user:${settings.peerName || currentUserName()}`) + const userPeerId = deriveUserPeerId(settings) const rootAgentPeerId = normalizeId(settings.aiPeer || "opencode") const activeAgentPeerId = rootAgentPeerId const childAgentPeerId = null @@ -1667,6 +1670,7 @@ export const createHonchoRuntimePlugin = export const HonchoRuntimePlugin = createHonchoRuntimePlugin() export const __testing = { createSessionState, + deriveUserPeerId, deriveSessionStateKey, extractCompletedAssistantMessage, honchoSdkImportPath: "@honcho-ai/sdk", diff --git a/tests/context-injection.test.js b/tests/context-injection.test.js index c2712ae..0c5e9d6 100644 --- a/tests/context-injection.test.js +++ b/tests/context-injection.test.js @@ -96,7 +96,7 @@ const createHonchoFetch = ({ failStableHydration = false } = {}) => { return jsonResponse({ peer_id: peerId, target_id: null, - representation: peerId.startsWith("user-") + representation: peerId === "user" ? "The user prefers concise engineering analysis." : "The assistant is working on opencode-honcho.", peer_card: ["Keep changes narrowly scoped."], diff --git a/tests/peer-topology.test.js b/tests/peer-topology.test.js index 02167d8..66abe01 100644 --- a/tests/peer-topology.test.js +++ b/tests/peer-topology.test.js @@ -5,7 +5,7 @@ import { __testing } from "../dist/index.js" test("root sessions keep user and root agent as peers", () => { const topology = __testing.buildPeerTopology({ config: {}, - userPeerId: "user:user", + userPeerId: "user", rootAgentPeerId: "opencode", activeAgentPeerId: "opencode", childAgentPeerId: null, @@ -13,7 +13,7 @@ test("root sessions keep user and root agent as peers", () => { }) expect(topology.sessionPeerConfigs).toEqual({ - "user:user": { observeMe: true, observeOthers: false }, + "user": { observeMe: true, observeOthers: false }, opencode: { observeMe: true, observeOthers: true }, }) expect(topology.describedPeers.childAgentPeer).toBeNull() @@ -23,7 +23,7 @@ test("root sessions keep user and root agent as peers", () => { test("classic peer model keeps delegated sessions on the Claude-style user and ai peers", () => { const topology = __testing.buildPeerTopology({ config: {}, - userPeerId: "user:user", + userPeerId: "user", rootAgentPeerId: "opencode", activeAgentPeerId: "opencode:reviewer", childAgentPeerId: "opencode:reviewer", @@ -31,7 +31,7 @@ test("classic peer model keeps delegated sessions on the Claude-style user and a }) expect(topology.sessionPeerConfigs).toEqual({ - "user:user": { observeMe: true, observeOthers: false }, + "user": { observeMe: true, observeOthers: false }, opencode: { observeMe: true, observeOthers: true }, }) expect(topology.describedPeers.childAgentPeer).toBeNull() diff --git a/tests/user-peer-id.test.js b/tests/user-peer-id.test.js new file mode 100644 index 0000000..fb29f6c --- /dev/null +++ b/tests/user-peer-id.test.js @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test" + +import { __testing } from "../dist/index.js" + +test("user peer id is the bare peerName with no prefix", () => { + expect(__testing.deriveUserPeerId({ peerName: "rui" })).toBe("rui") +}) + +test("user peer id falls back to the current user name when peerName is empty", () => { + expect(__testing.deriveUserPeerId({ peerName: "" })).toBe("user") +}) + +test("user peer id is normalised", () => { + expect(__testing.deriveUserPeerId({ peerName: "Rui Rei" })).toBe("rui-rei") +}) From b8d43de7c9cd6662a94ae24641deac6b2fef1836 Mon Sep 17 00:00:00 2001 From: Rui Rei Date: Fri, 5 Jun 2026 17:14:01 +0100 Subject: [PATCH 2/2] fix: reject configs where the user and agent peers collide Removing the user: prefix means peerName and aiPeer can now normalise to the same peer id (e.g. both "opencode"), silently merging user and agent memory into one peer. Fail fast on that collision instead. --- src/index.ts | 10 ++++++++++ tests/peer-collision.test.js | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/peer-collision.test.js diff --git a/src/index.ts b/src/index.ts index 23a41d8..d2f2684 100644 --- a/src/index.ts +++ b/src/index.ts @@ -619,6 +619,14 @@ const currentUserName = () => "user" const deriveUserPeerId = (settings: Pick) => normalizeId(settings.peerName || currentUserName()) +const assertDistinctUserAndAgentPeers = (userPeerId: string, rootAgentPeerId: string) => { + if (userPeerId === rootAgentPeerId) { + throw new Error( + `Invalid Honcho config: peerName and aiPeer both resolve to the peer id '${userPeerId}'; they must differ so user and agent memory stay separate.`, + ) + } +} + const rootApiKey = (raw: Record) => { const legacyApiKey = typeof raw[LEGACY_API_KEY_FIELD] === "string" ? expandEnv(raw[LEGACY_API_KEY_FIELD] as string) : "" return legacyApiKey @@ -759,6 +767,7 @@ const deriveRuntimeHandle = async ( const workspaceId = normalizeId(settings.workspace || "opencode") const userPeerId = deriveUserPeerId(settings) const rootAgentPeerId = normalizeId(settings.aiPeer || "opencode") + assertDistinctUserAndAgentPeers(userPeerId, rootAgentPeerId) const activeAgentPeerId = rootAgentPeerId const childAgentPeerId = null const parentAgentObserverPeerId = null @@ -1671,6 +1680,7 @@ export const HonchoRuntimePlugin = createHonchoRuntimePlugin() export const __testing = { createSessionState, deriveUserPeerId, + assertDistinctUserAndAgentPeers, deriveSessionStateKey, extractCompletedAssistantMessage, honchoSdkImportPath: "@honcho-ai/sdk", diff --git a/tests/peer-collision.test.js b/tests/peer-collision.test.js new file mode 100644 index 0000000..6245713 --- /dev/null +++ b/tests/peer-collision.test.js @@ -0,0 +1,13 @@ +import { expect, test } from "bun:test" + +import { __testing } from "../dist/index.js" + +test("distinct user and agent peers are accepted", () => { + expect(() => __testing.assertDistinctUserAndAgentPeers("rui", "opencode")).not.toThrow() +}) + +test("colliding user and agent peers are rejected", () => { + expect(() => __testing.assertDistinctUserAndAgentPeers("opencode", "opencode")).toThrow( + /peerName and aiPeer both resolve to the peer id 'opencode'/, + ) +})