feat(broker+cli): topics — conversation scope within a mesh (v0.2.0)
Adds the third axis of mesh organization: mesh = trust boundary, group = identity tag, topic = conversation scope. Topic-tagged messages filter delivery by topic_member rows and persist to a topic_message history table for back-scroll on reconnect. Schema (additive): - mesh.topic, mesh.topic_member, mesh.topic_message tables - topic_visibility (public|private|dm) and topic_member_role (lead|member|observer) enums - migration 0022_topics.sql, hand-written following project convention (drizzle journal has been drifting since 0011) Broker: - 10 helpers (createTopic, listTopics, findTopicByName, joinTopic, leaveTopic, topicMembers, getMemberTopicIds, appendTopicMessage, topicHistory, markTopicRead) - drainForMember matches "#<topicId>" target_specs via member's topic memberships - 7 WS handlers (topic_create/list/join/leave/members/history/mark_read) + resolveTopicId helper accepting id-or-name - handleSend auto-persists topic-tagged messages to history CLI: - claudemesh topic create/list/join/leave/members/history/read - claudemesh send "#deploys" "..." resolves topic name to id - bundled skill teaches Claude the DM/group/topic decision matrix - policy-classify recognizes topic create/join/leave as writes Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
packages/db/migrations/0022_topics.sql
Normal file
66
packages/db/migrations/0022_topics.sql
Normal file
@@ -0,0 +1,66 @@
|
||||
-- Topics — conversational primitive within a mesh (v0.2.0).
|
||||
--
|
||||
-- Spec: .artifacts/specs/2026-05-02-v0.2.0-scope.md
|
||||
--
|
||||
-- Mesh = trust boundary. Group = identity tag. Topic = conversation scope.
|
||||
-- Three orthogonal axes; topics complement (don't replace) groups.
|
||||
--
|
||||
-- Three new tables in the `mesh` pg-schema:
|
||||
-- * mesh.topic — named topic per mesh (unique on mesh_id, name)
|
||||
-- * mesh.topic_member — per-member subscriptions, with last_read_at
|
||||
-- * mesh.topic_message — persistent encrypted history (used for human-
|
||||
-- touched topics; agent-only topics may opt out)
|
||||
--
|
||||
-- Two new pg enums:
|
||||
-- * mesh.topic_visibility = public | private | dm
|
||||
-- * mesh.topic_member_role = lead | member | observer
|
||||
--
|
||||
-- Additive — no breaking changes to existing tables. Safe to deploy before
|
||||
-- CLI/broker code knows about topics; the routing layer falls back to the
|
||||
-- existing peer/group/* targeting until topic-tagged messages arrive.
|
||||
|
||||
CREATE TYPE "mesh"."topic_visibility" AS ENUM ('public', 'private', 'dm');
|
||||
CREATE TYPE "mesh"."topic_member_role" AS ENUM ('lead', 'member', 'observer');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mesh"."topic" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"mesh_id" text NOT NULL REFERENCES "mesh"."mesh"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"visibility" "mesh"."topic_visibility" NOT NULL DEFAULT 'public',
|
||||
"created_by_member_id" text REFERENCES "mesh"."member"("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"archived_at" timestamp
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "topic_mesh_name_unique"
|
||||
ON "mesh"."topic" ("mesh_id", "name");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mesh"."topic_member" (
|
||||
"topic_id" text NOT NULL REFERENCES "mesh"."topic"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"member_id" text NOT NULL REFERENCES "mesh"."member"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"role" "mesh"."topic_member_role" NOT NULL DEFAULT 'member',
|
||||
"joined_at" timestamp DEFAULT now() NOT NULL,
|
||||
"last_read_at" timestamp
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "topic_member_unique"
|
||||
ON "mesh"."topic_member" ("topic_id", "member_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "topic_member_by_member"
|
||||
ON "mesh"."topic_member" ("member_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "mesh"."topic_message" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"topic_id" text NOT NULL REFERENCES "mesh"."topic"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"sender_member_id" text NOT NULL REFERENCES "mesh"."member"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"sender_session_pubkey" text,
|
||||
"nonce" text NOT NULL,
|
||||
"ciphertext" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Composite index for the common access pattern: load topic history
|
||||
-- ordered by time. Drives the web chat panel's infinite-scroll fetch.
|
||||
CREATE INDEX IF NOT EXISTS "topic_message_by_topic_time"
|
||||
ON "mesh"."topic_message" ("topic_id", "created_at");
|
||||
Reference in New Issue
Block a user