Skip to content

notes group integration: generic channel-host convention + membership#5897

Open
arthyn wants to merge 14 commits into
developfrom
notes-groups-integration
Open

notes group integration: generic channel-host convention + membership#5897
arthyn wants to merge 14 commits into
developfrom
notes-groups-integration

Conversation

@arthyn

@arthyn arthyn commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

This improves %groups ability to have non-%channels agents integrate with it as well as makes the integration with our client more "real" and less of a hack. This is in service of us trying to build an actual UI integration with %notes.

Companion to arthyn/notes#7

Changes

Adds support for %notes notebooks as a group channel type, via a generic "channel-host" convention so %groups can drive a backing agent it doesn't know about.

%groups agent

  • Generic channel-host convention: a nest whose kind isn't %chat/%diary/%heap is backed by the agent named by the kind. join-channels/leave-channels poke %<kind>-join/%<kind>-leave; is-joined reads a conventional /joined/<host>/<name> scry for active-channels. chat/diary/heap keep their existing %channels fast-path untouched. No %groups state change.
  • New group-channel-active poke (+ mar/group/channel-active) lets a backing agent report channel join/leave so active-channels stays current without %groups subscribing to it.

Client

  • Notes channels are created via the %notes HTTP API (POST /notes/~/v1/notebooks), which returns the server-assigned flag deterministically (added Urbit.requestJson + an api.requestJson wrapper).
  • Membership for notes channels is reconciled from each group's active-channels set (setJoinedNotesChannels, fed from initApi) rather than %channels' joined list, which doesn't track notes. setLeftGroupChannels is scoped to %channels-backed kinds.
  • Join/leave for notes channels are routed to %notes instead of %channels; %channels-only affordances that don't apply to notes are suppressed (hooks preview, mark-as-read).

How did I test?

  • %groups compiles against the develop groups desk; the %notes desk's unit suite passes (84/84), including the state migration.
  • Manually on a dev ship: create-in-group, join/leave a notes channel, and active-channels population (host- and subscriber-side).

Risks and impact

  • Safe to rollback without consulting PR author? Yes
  • Affects important code area:
    • Onboarding
    • State / providers
    • Message sync
    • Channel display
    • Notifications
    • Other: %groups agent (channel-host convention)

Additive: the generic channel-host path only fires for non-%channels kinds (unknown/uninstalled kinds no-op), and notes-specific client branches are gated on the notes/ id prefix, so chat/diary/heap behavior is unchanged. No %groups state migration.

Rollback plan

Revert the commits on this branch. There is no %groups state migration to unwind, and the client changes are gated to notes channels, so reverting restores prior behavior cleanly. The %notes companion desk is deployed separately and is inert for clients without these changes.

Screenshots / videos

arthyn added 4 commits June 2, 2026 16:45
Let %groups auto-join/leave and track active-channels for backing agents
other than %channels (e.g. %notes), without naming any of them.

Convention: a nest whose kind is not %chat/%diary/%heap is backed by the
agent literally named by the kind on our ship. %groups pokes it
%<kind>-join (carrying [nest group-flag]) / %<kind>-leave (carrying nest),
and reads a conventional .../joined/<host>/<name> scry for active-channels.
chat/diary/heap keep their existing %channels fast-path untouched.

- join-channels / leave-channels: generic else-branch building the
  %<kind>-join/-leave poke to [our kind]. Uninstalled kinds just nack
  (logged in go-agent, harmless).
- is-joined helper + the two active-channels scry sites (group init,
  channel-add repair) gain the generic /joined branch.

No %groups state change. Compiles against the develop groups desk.
Clean-cut removal of the three notes-channel hacks now that %notes is
group-aware and %groups drives it via the channel-host convention.

- createNotesChannel: create the notebook through the %notes HTTP API
  (POST /notes/~/v1/notebooks with {title, group}), reading the
  server-assigned flag straight from the response — no poke+poll, no
  forced-public visibility. Binds the notebook to the group.
- add Urbit.requestJson + an api.requestJson wrapper (authed JSON request to
  an arbitrary ship path; reauth-on-403) for first-class agent HTTP APIs.
- initApi: fold each group's active-channels into joinedChannels so channels
  backed by non-%channels agents (notes) reconcile membership normally.
- queries.ts setLeftGroupChannels: drop the notes/% force-join special-case;
  notes channels now reconcile like any other channel.

api + shared typecheck clean.
…-channels

Correction to the previous client commit. toClientChannel already computes
currentUserIsMember kind-agnostically from group reader-roles (open channel or
the user holds a reader role) and sets type/contentConfiguration — for notes
channels just like chat/diary/heap, on every group sync. So notes membership is
already handled the same way as all group channels.

The only real issue was that setLeftGroupChannels (the %channels-membership
reconcile) flipped group channels not in joinedChannels to not-a-member,
clobbering that reader-role membership for notes (which %channels doesn't track).

- setLeftGroupChannels: scope the flip to %channels-backed kinds
  (chat/diary/heap) so notes (and any future channel-host kind) keep the
  membership toClientChannel derived. Replaces both the old force-join hack and
  the active-channels approach.
- initApi: revert the active-channels -> joinedChannels fold. active-channels
  remains a %groups-internal signal that drives backend auto-join (Part B); it
  is not the client's membership source.

api + shared typecheck clean.
- %groups: generic group-channel-active poke (new mar/group/channel-active)
  lets a backing agent (e.g. %notes) report channel join/leave so %groups can
  keep active-channels current without subscribing to it; resolves the group
  from the poke and no-ops if absent. is-joined uses %gu for all kinds; the
  %add repair reuses it.
- client: notes channels created via the %notes HTTP API (deterministic flag);
  membership reconciled from each group's active-channels (setJoinedNotesChannels,
  fed from initApi) rather than %channels' joined list, which doesn't track
  notes. setLeftGroupChannels scoped to %channels-backed kinds.

api + shared typecheck clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c5ce48d93

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/shared/src/store/channelActions.ts
- useChannelHooksPreview: skip notes channels (hooks are a %channels feature;
  /v1/hooks/preview bad-watch-paths on %channels for a notes nest).
- leaveGroupChannel: notes channels leave via a %notes poke (unsubscribe +
  report leave to %groups) instead of poking %channels, which rejects the
  unknown nest.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

await api.joinChannel(channelId, groupId);

P2 Badge Route notes channel joins through %notes

When a user has left a notes/... group channel, the Join action still calls api.joinChannel(channelId, groupId), which pokes %channels. This patch already special-cases leaving because notes channels are not %channels-backed, so the same notes nest is rejected here and the catch block rolls currentUserIsMember back to false; that leaves users unable to rejoin notes channels they intentionally left.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

arthyn added 2 commits June 4, 2026 12:57
- joinGroupChannel: notes channels join via a %notes poke (subscribe + report
  to %groups) instead of poking %channels.
- ChatOptionsSheet: hide "Mark as read" for notes channels — they have no
  %channels/%activity unreads, so it doesn't apply (and would poke %activity
  with an unknown source).

(Channel meta/rename already works for notes via api.updateChannel → %groups;
only the writers sub-pokes hit %channels and are left as-is for now.)
Note that the channel-host active-channels report comes from third-party
agents like %notes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c9ed8f7a9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread desk/app/groups.hoon Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0d45502a4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread desk/app/groups.hoon
The %group-channel-active poke arm was added without declaring the mark, so
the discipline wrapper rejected the poke before the arm ran (active-channels
stayed stale and notes membership never reconciled for other clients).

- import the mark (/% m-group-channel-active) and add it to the discipline's
  poke-mark allow-list.
- add a named $channel-active type to the groups sur and use it as the mark's
  sample — discipline marks need a single named type (an inline tuple fails
  the /% build / vale derivation).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49f500a6b5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const res = await api.requestJson<NotesCreateResponse>(
'/notes/~/v1/notebooks',
'POST',
{ title, group: { host: groupHost, flagName: groupName } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve notes channel descriptions on create

When a notes channel is created with a description, this request only sends title and group to %notes; the description is only inserted optimistically into the local DB. Since the backend-created group channel has no description to return on the next init/sync, the user's description is lost after refresh, unlike the previous addChannelListingToGroup path which sent it in meta.description.

Useful? React with 👍 / 👎.

arthyn and others added 3 commits June 4, 2026 16:22
createNotesChannel dropped the `readers` it received, so the POST body
only carried {title, group} and the %notes host registered the group
channel with no reader roles — every notes channel created open,
defeating the group's can-read gate for private channels.

Thread `readers` from createChannel through createNotesChannel into the
POST body; the %notes host registers the group channel with the correct
reader roles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Joining/leaving a channel-host channel (e.g. %notes) mutated the group's
active-channels set but never notified subscribers, so a second same-ship
client only learned the change on its next full init sync.

Add a local-only %active-channels r-group response variant carrying the
per-nest membership delta. The %group-channel-active poke arm now emits it
on /v1/groups (a from-self-only path, so it never crosses the wire) after
updating active-channels. The client reduces it to the existing
join/leave GroupUpdate, reconciling membership without a re-sync.

- sur/groups + groups-ver (v9): new [%active-channels =nest joined=?] arm
- groups-conv: drop it from v7/v2 backcompat diff (compile guard)
- groups-json: encode { active-channels: { nest, joined } }
- groups.hoon: relax discipline on group-response-1 (its carried type
  changed; must not pin into version negotiation) + emit the local fact
- client: parse the variant into joinChannel/leaveChannel

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Factor the active-channels mutation + local %active-channels emit into a
shared set-active-channel helper and route the take-channels create/join/
leave paths through it too, not just the %notes %group-channel-active arm.

active-channels is now the single authoritative membership signal for
every channel kind — a second same-ship client reconciles join/leave for
built-in chat/diary/heap channels the same way it does for notes, without
relying on a separate cross-reference to %channels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6846255fed

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1540 to +1542
return joined
? { type: 'joinChannel', channelId: nest, groupId }
: { type: 'leaveChannel', channelId: nest };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep active-channel joins membership-only

When a %active-channels join arrives for a notes channel that is not already in a local nav section (for example a same-ship client just created or joined it), this is converted into the existing joinChannel update. handleGroupUpdate processes that via db.addJoinedGroupChannel, which does more than flip membership: if no section row is present it inserts a group_nav_section_channels row under the bare groupNavSectionId 'default' even though group section IDs are stored as ${groupId}-${sectionId}. That can fail the FK or leave an invalid nav-section entry for a membership-only delta; consider a separate update path that only sets currentUserIsMember.

Useful? React with 👍 / 👎.

A %channel %edit r-group update is reduced into a db.Channel via
toClientChannel, which has no role context in the reducer and so computes
currentUserIsMember from readers ∩ {} — false for any role-restricted
channel. The updateChannel apply step then wrote that false, making a
channel an admin had just restricted appear "left" until a full refresh
(insertChannels excludes currentUserIsMember on upsert, so the forced
syncGroup couldn't repair it).

currentUserIsMember is the joined flag (addJoinedGroupChannel /
removeJoinedGroupChannel set it) and is owned by the full group sync
(which has the user's roles) and the active-channels join/leave path. A
channel edit must not redefine it, so strip it from the updateChannel
apply. Previously latent — only open channels (readers=[] → member=true)
were reachable; role-restricted notes/group channels now expose it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2da4a0242

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread desk/app/groups.hoon
:: scry with a loob.
=/ =path
/(scot %p our.bowl)/[p.nest]/(scot %da now.bowl)/joined/(scot %p p.q.nest)/[q.q.nest]
.^(? %gu path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard generic membership scries before reading them

For any non-built-in channel kind whose backing agent is missing or does not implement /joined/<host>/<name>, this naked %gu scry can crash the groups agent during init/channel creation paths that call is-joined. The previous code skipped non-%channels kinds entirely, and other scries in this desk first check the target agent/path (for example with /$) before reading a value; add the same existence/fallback handling here so unsupported custom channel kinds read as not joined instead of aborting the update.

Useful? React with 👍 / 👎.

@Fang- Fang- left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed out of band, I don't hate this. Some comments/questions below. In particular, not clear to me why we would want to do dynamic marks for the status update pokes that come out of groups agent.

Comment thread desk/app/groups.hoon Outdated
Comment thread desk/app/groups.hoon Outdated
Comment thread desk/app/groups.hoon Outdated
Per review (Fang): the per-kind dynamic marks for the channel-host
join/leave pokes aren't needed — the nest kind already routes the poke to
the backing agent, and the payload shape/meaning are fixed. Replace the
(crip "{kind}-join") construction with static %group-channel-join /
%group-channel-leave.

Make the convention's marks first-class on the groups desk, symmetric with
%group-channel-active: add channel-join/channel-leave types to sur/groups,
new mar/group/channel-{join,leave}.hoon, and register both in the agent
discipline. Pokes are cast to the canonical types.

Also tidies the set-active-channel / channel-active doc comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arthyn arthyn requested a review from Fang- June 8, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants