A reply routinely fanned out to every session of the sender's member, which is the "peers spread messages to the wrong peers" noise. Three links fed each other: - channel meta put the MEMBER key in from_pubkey/from_id, and the skill told the model to reply by from_name (repeatable across sessions). from_pubkey/from_id are now the sender's session key; the member key moves to from_member_pubkey; the skill says reply to from_pubkey. - a DM to a member key was claimed by that member's daemon socket and shown to all its sessions. New daemon/resolve-target.ts is the single resolver for /v1/send: a member key maps to its only live session, is refused (409 member_target_ambiguous + candidates) when it has several, and needs --fanout to reach all of them on purpose. A duplicated display name or prefix is ambiguous instead of first-match; control- plane rows are never addressable. - a session MCP whose token didn't resolve fell into the unfiltered SSE stream and rendered every sibling's DMs. The MCP now subscribes with scope=session and gets no message events without a resolved session. Also: --self lists only the current mesh and requires a registered session (it could message its own new row); a DM to a control-plane row says so instead of the wrong "session pubkeys are ephemeral" hint, and `peer list --all` tags those rows (bug9). Spec: .artifacts/specs/2026-09-26-session-identity-and-routing.md §2-3, §6 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
/**
|
|
* Spec 2026-09-26 §3: the "messages land on the wrong peers" noise.
|
|
* A member-key DM reached every session of that member; a duplicated
|
|
* display name silently picked the first row; control-plane rows were
|
|
* addressable. One resolver now decides for every /v1/send caller.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { resolveDirectTarget, type RosterPeer } from "~/daemon/resolve-target.js";
|
|
|
|
const M1 = "a".repeat(64);
|
|
const M2 = "b".repeat(64);
|
|
const S1 = "1".repeat(64);
|
|
const S2 = "2".repeat(64);
|
|
const S3 = "3".repeat(64);
|
|
|
|
const roster: RosterPeer[] = [
|
|
{ pubkey: M1, memberPubkey: M1, displayName: "daemon", role: "control-plane" },
|
|
{ pubkey: S1, memberPubkey: M1, displayName: "Intra-Back" },
|
|
{ pubkey: S2, memberPubkey: M1, displayName: "Intra-Back" },
|
|
{ pubkey: S3, memberPubkey: M2, displayName: "portal-kit" },
|
|
];
|
|
|
|
describe("resolveDirectTarget", () => {
|
|
it("a session pubkey resolves to itself", () => {
|
|
expect(resolveDirectTarget(S1, roster)).toMatchObject({ ok: true, pubkey: S1 });
|
|
});
|
|
|
|
it("a member key with one live session resolves to that session", () => {
|
|
expect(resolveDirectTarget(M2, roster)).toMatchObject({ ok: true, pubkey: S3, note: "member_to_session" });
|
|
});
|
|
|
|
it("a member key with several sessions is refused, listing them", () => {
|
|
const r = resolveDirectTarget(M1, roster);
|
|
expect(r.ok).toBe(false);
|
|
if (!r.ok) {
|
|
expect(r.code).toBe("member_target_ambiguous");
|
|
expect(r.candidates.map((c) => c.pubkey).sort()).toEqual([S1, S2]);
|
|
}
|
|
});
|
|
|
|
it("--fanout keeps the member key on purpose", () => {
|
|
expect(resolveDirectTarget(M1, roster, { fanout: true })).toMatchObject({ ok: true, pubkey: M1, note: "member_fanout" });
|
|
});
|
|
|
|
it("an unknown full key stays (offline store-and-forward)", () => {
|
|
const k = "f".repeat(64);
|
|
expect(resolveDirectTarget(k, roster)).toMatchObject({ ok: true, pubkey: k });
|
|
});
|
|
|
|
it("a duplicated display name is ambiguous, not first-match", () => {
|
|
expect(resolveDirectTarget("intra-back", roster)).toMatchObject({ ok: false, code: "ambiguous" });
|
|
expect(resolveDirectTarget("portal-kit", roster)).toMatchObject({ ok: true, pubkey: S3 });
|
|
});
|
|
|
|
it("control-plane rows are never addressable by name", () => {
|
|
expect(resolveDirectTarget("daemon", roster)).toMatchObject({ ok: false, code: "not_found" });
|
|
});
|
|
|
|
it("a prefix prefers session keys and must be unique", () => {
|
|
expect(resolveDirectTarget("3333", roster)).toMatchObject({ ok: true, pubkey: S3 });
|
|
// member-key prefix of a member with two sessions → ambiguous
|
|
expect(resolveDirectTarget("aaaa", roster)).toMatchObject({ ok: false, code: "ambiguous" });
|
|
expect(resolveDirectTarget("bbbb", roster)).toMatchObject({ ok: true, pubkey: S3 });
|
|
});
|
|
});
|