diff --git a/src/index.ts b/src/index.ts index f19e669..d2f2684 100644 --- a/src/index.ts +++ b/src/index.ts @@ -616,6 +616,17 @@ const writeSettings = async ( 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 @@ -754,8 +765,9 @@ 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") + assertDistinctUserAndAgentPeers(userPeerId, rootAgentPeerId) const activeAgentPeerId = rootAgentPeerId const childAgentPeerId = null const parentAgentObserverPeerId = null @@ -1667,6 +1679,8 @@ export const createHonchoRuntimePlugin = export const HonchoRuntimePlugin = createHonchoRuntimePlugin() export const __testing = { createSessionState, + deriveUserPeerId, + assertDistinctUserAndAgentPeers, 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-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'/, + ) +}) 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") +})