groups: add group-level custom blob field - #6209
Conversation
Add an opaque, admin-writable blob=(unit @t) to $group for client-defined metadata, mirroring the channel-level custom meta lane. New v11 types carry %blob on a-group/c-group/u-group/r-group; responses ride a new /v3/groups lane (group-response-3) with conv downgraders keeping v2/v1/v0 blob-free. Actions ride a new strict group-action-5 mark; group-action-4 stays frozen at v8. State bumps %11 -> %12 (blob defaults to ~, pub logs re-walked) and the groups negotiate version bumps %2 -> %3 since the update/log/command wire shapes change. New read surfaces: /x/v3 group scries, /x/v4/init, /v9/init and /v10/changes on %groups-ui. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire types for the %blob action/response, updateGroupBlob poke via group-action-5 tracked on /v3/groups, a third groups subscription filtered to blob events, getGroup/getGroups moved to the v3 scries, init/changes bumped to /v9/init and /v10/changes, a groups.blob db column, editGroupBlob sync handling, and an optimistic updateGroupBlob store action. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's tsc config rejects dynamic import() (TS1323). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getGroups/getGroup, /v9/init, and /v10/changes 404 against backends that haven't shipped the group blob; retry the v2/v7/v8 paths, whose responses are identical minus the blob. Softens the hard cutover for not-yet-OTA'd nodes and lets the bot e2e harness run against its archived ships. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87d3a61 to
a13c04e
Compare
Joining a group whose blob predates the join emits %create on both lanes: v3 carries the blob, v1 strips it. Process v3 %create in the blob subscription and treat a missing blob key as "don't touch" in the group upsert (explicit null still clears), so the v1 copy that lands second no longer clobbers the blob to null. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the current action mark first in the dispatch; the deprecated arms read as a descending tail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
%group-ui-3 is the singular per-group mark (/x/v3/ui/groups/$/$) but sat after %groups-ui-2, reading as though it belonged to the plural groups-ui family. Move it beside %group-3. The strict list is gassed into a set, so ordering is cosmetic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
%group-ui, -1 and -2 were never in the discipline strict list, though they existed and were rail-registered when the list was written. That left the guardrail protecting the plural groups-ui family (which has no consumer) while the singular family read by the frontend getGroup and ted/group/create* went unchecked. Add them alongside the new %group-ui-3. Adding is safe on upgrade: check-marks iterates the stored mark map, so a newly listed mark isn't compared until the load after it is first snapshotted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
changesApi scried /v10/changes while annotating the result ChangesV8, and initApi scried /v9/init as GroupsInit7. Both were structurally right (the blob rides inside GroupV7) but read a version behind. Follow the convention already in urbit/ui.ts — interface named for the newest version, older ones aliased to it — so the primary types become ChangesV10 and GroupsInit9 and the fallback branches keep the older names that match the paths they actually scry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client opened /v1 unfiltered plus filtered /v2 and /v3 concurrently, so adding %create to the v3 filter meant every create was handled twice and the blob upsert guard became load-bearing mitigation for that collision. r-group:v11 is a superset of v9 and v10, so subscribe to /v3/groups alone and handle everything from it. Only when that watch path is unavailable do we open the legacy v1 + v2 pair. No event is handled twice, and the undefined-vs-null blob distinction goes back to being defensive rather than propping up a race. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
groupAction5 was annotated GroupActionV4, and because blob had been added to the shared inner GroupAction type, all 25 groupAction4 call sites also accepted a blob action — which mark group-action-4 cannot carry, since its dejs is a-groups:v8. That typechecked and would have nacked at runtime. Parameterize the envelope, which is identical at both versions, on the a-group payload: GroupActionV4 keeps the blob-free GroupAction while GroupActionV5 adds it. GroupActionV4 now rejects a blob action at compile time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
The desk ships ahead of the app, so a client newer than its backend is not a state we support; the v2/v7/v8 fallbacks protected a direction that cannot happen and had no precedent here (prior init bumps were bare path swaps). Removing them also deletes the one code path that was never verified against a real old ship. The backend keeps its v0/v1/v2 downgrade lanes untouched — those serve old clients on a new desk, which is the normal state. Also repoints the five tracked group pokes from /v2/groups to /v3/groups. A trackedPoke only resolves when its watch endpoint's own subscription receives an event, so after collapsing to one lane they were watching a path nothing subscribes to and every group mutation would have hung 20s then thrown. Adds a test that asserts tracked pokes watch a subscribed lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It restated a ten-line function rather than asserting anything the code does not already say, and would need editing on every version bump. The tracked-poke test still covers the lane invariant that actually matters, since that one relates two independent places in the file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audited every version-tagged type the blob change touched, directly or
transitively:
V1GroupResponse -> GroupResponse named for the /v1 path we no longer
read, on a type that is now v11, and
the only versioned name among its ten
GroupResponse* siblings
GroupV7 -> GroupV11 gained blob
GroupsV7 -> GroupsV11 map of the above
toV1GroupsUpdate -> toGroupsUpdate
toClientGroupV7 -> toClientGroup one wire version reaches these
toClientGroupsV7 -> toClientGroups now, so drop the suffix
extractChannelReadersFromV7Groups -> extractChannelReadersFromGroups
Deletes ChangesV8, ChangesV7, GroupsInit7 and GroupsInit6, which were
aliases kept only for the fallbacks; the surviving fixture casts point
at GroupsInit9.
Left alone: GroupChannelV7, SectionV7, SectionsV7 and GroupPreviewV7
describe shapes that are unchanged from v7 through v11, so their names
are accurate and renaming them would make them wrong.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Version arithmetic like "v10 changes is v8 plus the group blob" is already carried by the type names and the paths beside them. Two of these were also stale, still citing the v1/v2 fallbacks that were removed. What stays is the non-obvious: why group-action-4 would nack a blob, why undefined and null differ in the group upsert, why tracked pokes must watch the subscribed lane, and the Hoon type names each wire type maps to, which TypeScript cannot show. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The client already hard-requires the new desk — getGroups, getGroup and subscribeGroups all read /v3 with no fallback — so keeping action-4 bought no compatibility while leaving a live footgun about which mark can carry which action. Moves all 29 pokes to group-action-5: the 25 in groupsApi via a single merged groupAction() helper, plus groupNavigationBatchUpdate, the flag-content poke in postsApi, four raw pokes in tlon-skill, and the web mock. The Hoon %group-action-4 arm stays for older app builds. With one live mark the GroupActionEnvelope<T> parameterization has no purpose, so it collapses back to a concrete GroupActionV5 over a single GroupAction that includes blob; GroupActionV4 and GroupActionV5Data are gone. Adds dejs tests in tests/lib/groups-json proving the migrated actions still parse at v11 — meta, delete, entry/privacy and leave — alongside blob set and clear. v11's a-group dejs is v8's arm list plus blob+(mu so), with identical sub-parsers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
mikolajpp
left a comment
There was a problem hiding this comment.
This looks good.
Some nits in the comments. Appreciate the less-verbose coding style :-)
| [%x ver=?(%v0 %v1 %v2) %light %groups ~] | ||
| =/ groups-9=groups:v9:gv | ||
| %- ~(run by groups) | ||
| |= [=net:v9:gv =group:v9:gv] | ||
| (drop-seats:group:v9:gc group our.bowl) | ||
| |= [=net:g =group:g] | ||
| (v9:group:v11:gc (drop-seats:group:v11:gc group our.bowl)) | ||
| ?- ver.pole | ||
| %v0 ``groups+(~(run by groups-9) v2:group:v9:gc) | ||
| %v1 ``groups-1+(~(run by groups-9) v5:group:v9:gc) | ||
| %v2 ``groups-2+groups-9 |
There was a problem hiding this comment.
Is this endpoint in use by anything? If not, we should mark it for removal later. Otherwise it needs updating too.
There was a problem hiding this comment.
Checked — nothing reads it: no reference to /x/vN/light/groups or the /x/groups/light alias anywhere in packages/, apps/, or desk/. Deleted outright rather than marked, in the stacked #6224.
| %v1 ``groups-ui-1+(~(run by groups) group-ui:v5:group:v11:gc) | ||
| %v2 ``groups-ui-2+(~(run by groups) group-ui:v9:group:v11:gc) |
There was a problem hiding this comment.
Same comment as above, we don't expose v11 here.
There was a problem hiding this comment.
Same story: the only reference to the plural /x/vN/ui/groups map was a request-timeout test using the path as filler. The in-desk create threads read the per-flag /gx/groups/vN/ui/groups/<ship>/<name>, which is a different arm and stays. So rather than expose v11 there, #6224 deletes the plural map. Left unresolved for your call.
|
Seems good to me, my stuff could write to this easily |
- delete lib/mark-warmer.hoon and its two imports; on kelvin 408 all marks build by default, so pre-building them is redundant - deflate %group-action-4 into %group-action-5 with the $(+< ...) pattern %group-action-3 already uses, rather than duplicating the dispatch; a-groups:v8 nests under v11 - add %group-action-4 to the strict mark list, which it was missing - drop two casts the typed rails make unnecessary - give both downgrade lanes in +go-response the same ?~ shape; v10's nil case (%blob) is a subset of v9's (%blob, %active-channel), so returning early on v2 cannot skip a v1 send Verified on a moon: an old %group-action-4 poke applies through the v5 handler, and blob still reaches /v3 while /v2 stays blob-free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f3d6e73fce
ℹ️ 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".
zod, ten and mug are re-archived from ships running this branch's desk, so the e2e harnesses can talk to a client that reads /v3 with no fallback. Verified the archives carry it: mar/group-3, mar/group/action-5 and blob=(unit @t) in sur/groups.hoon. ~bus stays at rube-bus9, still deliberately outdated for protocol mismatch testing. Named after what they contain rather than a position in the numbered lineage, via a new ARCHIVE_TAG override in archive-piers.sh. Note the tag is now required for the next bump: the script errors out rather than silently minting rube-zod1.tgz when it meets a non-numeric manifest entry. Also updates the bot e2e compose, which archive-piers.sh does not touch and which is what Shared E2E actually reads — including the pinned md5 and byte size for each ship, cross-checked against gsutil stat. Old archives stay in the bucket, so openclaw's dev compose and its rube-27-calibrated gateway-status test keep working untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neither surface has a consumer: nothing anywhere reads /x/vN/light/groups
or the deprecated /x/groups/light alias, and the only reference to the
plural /x/vN/ui/groups map was a request-timeout test using the path as
arbitrary filler (the in-desk create threads read the per-flag path,
which stays). Rather than carry them forward to v3, remove the peek arms
and their scry declarations.
Repoints that timeout test at /v3/groups so it does not name a deleted
endpoint.
Leaves the now-unproduced %groups-ui{,-1,-2} marks in place: dropping a
strict mark is a one-way door and keeping them costs nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Case 08's first readStatus() died with a terminal 404 as soon as this branch regenerated the e2e piers. The scry it reads moved months ago: the steward refactor (3b4d55b and follow-ups) relocated gateway liveness state into %steward's gateway module and reduced %gateway-status to a poke-only proxy whose on-peek is a stub. The case only kept passing because it was reading a rube-27 pier baked from a pre-refactor desk, so any pier refresh would have surfaced this. Point the read path at %steward /x/v1/gateway/status. The write path is untouched — the plugin still pokes %gateway-status-action-1, which the proxy forwards on as steward-gateway-action-1. decodeGatewayStatus needs no change: ga-peek serves the same [status (unit @da)] tuple. Verified against a live ship on the current desk — /v1/gateway/status cues and decodes to {status, leaseUntil}, /v1/gateway/owner-activity decodes via decodeDa, and the old %gateway-status path returns the same terminal 404 seen in CI. Also moves openclaw's dev compose to the group-blob archives. Both it and tlon-bot-e2e's compose run test/cases/, so leaving dev on rube-27 would trade a CI failure for a `pnpm test:integration` failure. md5s and byte sizes cross-checked against the x-goog-hash headers on bootstrap.urbit.org. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6692f1f6f3
ℹ️ 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".
updateGroupBlob: the host's %blob arm emits nothing when the value is
unchanged, so trackedPoke waited out its 20s timeout and then rolled
back a write that was already correct. Return early instead. Adds tests
covering unchanged, both-empty, changed and cleared; the two no-op cases
fail without the guard.
archive-piers.sh: the version guard added with ARCHIVE_TAG never fired.
print_error/print_info write to stdout, and `local x=$(f)` returns the
status of local rather than f, so the caller captured the diagnostics as
the version string and would have built an archive name out of the error
text. Redirect those two prints to stderr, matching archive_pier's
existing convention, and split the declaration from the assignment so
the failure aborts.
update_dockerfile matched only rube-{ship}[0-9]*.tgz. That worked for
the numeric-to-tagged transition but would silently skip a second tagged
run, leaving the Dockerfile on a stale tarball. Match any suffix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a085007743
ℹ️ 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".
…ld-custom-metadata' into hunter/tlon-6274-remove-dead-group-scry-surfaces
Deleting `/x/groups/light` and the `%light %groups` arm broke CI: rube's
`checkGroupsAppHealth` probes `/~/scry/groups/groups/light.json`
(apps/tlon-web/rube/index.ts:1205), so every ship failed its readiness
check and both the parallel E2E job and the production build smoke test
timed out in "Setting up ships".
Three more live callers depend on the same deprecated alias:
- apps/tlon-mobile/ios/Shared/Networking/PocketChatAPI.swift:17
- apps/tlon-mobile/android/.../api/TalkApi.java:137
- api/openapi.yaml:42, documented as /groups/groups/light.json
The earlier audit missed them by grepping the versioned spelling
(`light/groups`); every real caller uses the alias spelling
(`groups/light`).
The plural `%ui %groups` arm is still genuinely dead and stays deleted --
its only references were the per-group `/vN/ui/groups/{ship}/{name}` arm,
which survives, and a timeout test that used the path arbitrarily.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGt4ADrmVsoD8mxunqeZxp
#6188 claims `[%x %v9 %init ~]` and `[%x %v10 %changes ~]` for v10-native activity, and adds its own `mar/ui/init-9.hoon` and `sur/ui.hoon` `init-9`. Blob ships after activity, so it takes the next numbers instead of folding into those arms: /v9/init -> /v10/init init-9 -> init-10 /v10/changes -> /v11/changes %ui-init-9 -> %ui-init-10 GroupsInit9 -> GroupsInit10 ChangesV10 -> ChangesV11 Still on `activity:v8:av` because `v10` does not exist until #6188 merges. That flip is required, not cosmetic: `v9:source:v10:ac` returns `~` for `%note` and `%notebook`, so a v8-sourced init or changes silently drops every note unread. Three sites to change on rebase — `sur/ui.hoon` `init-10`, `groups-ui.hoon` `/v4/activity` in both arms, and `mar/ui/init-10.hoon`'s enjs call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGt4ADrmVsoD8mxunqeZxp
…paque-blob-field-custom-metadata Notes activity (#6188, #6200, #6256) landed, so the blob init/changes arms stack on top of it and adopt v10-native activity. sur/ui.hoon keep develop's init-9; init-10 = groups-ui:v11 + activity:v10 groups-ui.hoon keep /v9/init and /v10/changes; /v10/init and /v11/changes now scry /v6/activity instead of /v4 mar/ui/init-10 enjs activity:v8 -> activity:v10 The activity flip is required, not cosmetic: v9:source:v10:ac returns ~ for %note and %notebook, so a v8-sourced init or changes drops every note unread. initApi/changesApi hard cutover to /v10/init and /v11/changes, dropping develop's getActivitySupportsNotes() fallback tier piers take develop's rube-*29; the -group-blob archives predate notes activity, and rube applies the desk delta on top gateway-status take develop's; %gateway-status is gone now, superseding the intermediate poke-proxy fix on this branch Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGt4ADrmVsoD8mxunqeZxp
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae68435664
ℹ️ 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".
init-10:u carries activity:v10:av from /v6/activity, but the mark still serialized it with activity:v8:enjs. v10 adds %notebook and %note to $source, so v10 does not nest in v8 — the mark could not compile, and had it compiled it would have dropped note unreads from the init JSON. The flip was made while resolving the develop merge but never staged: the file was not among the conflicted paths, so `git add` of the conflict list missed it and the merge committed the index. The Hoon suite passed because run-tests.sh rsyncs the working tree rather than HEAD. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGt4ADrmVsoD8mxunqeZxp
…roups-add-group-level-opaque-blob-field-custom-metadata
…paque-blob-field-custom-metadata
…paque-blob-field-custom-metadata
The harness boots ~zod, ~ten and ~mug (docker-compose.base.yml serves mug
on 8082 from rube-mug29.tgz) but only committed the branch desk onto the
first two, leaving ~mug on whatever desk its pier archive shipped with.
A mixed-version fleet is harmless until a branch bumps the negotiated
%groups protocol version. This one does (~.groups %2 -> %3, since u-group
%create embeds $group), so ~zod and ~ten classify ~mug as a
certain-mismatch, and negotiate does not defer a poke to a mismatching
gill -- lib/negotiate.hoon:295-302 crashes outright:
~| [%negotiate %poke-to-mismatching-gill gill]
!!
~mug is the reel/lure provider, so every scenario touching group or channel
setup died while DM-only scenarios passed: 10 failed, 12 passed.
The failure was near-undiagnosable because the crash arrives through a
spider thread whose outputMark is json, and mar/tang.hoon is not in
peru.yaml's pick list -- so %tang -> %json has no cast and the tang never
reaches the caller. Only the ship console carried the hint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGt4ADrmVsoD8mxunqeZxp
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f778ccdfae
ℹ️ 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 response = await scry<ub.ChangesV11>({ | ||
| app: 'groups-ui', | ||
| path: `/${changesVersion}/changes/${encodedTimestamp}`, | ||
| path: `/v11/changes/${encodedTimestamp}`, |
There was a problem hiding this comment.
Route iOS cached changes through v11
In the iOS background-cache path, apps/tlon-mobile/ios/Shared/Networking/ChangesAPI.swift still requests /v10/changes (or /v8/changes), so a blob update occurring while the app is backgrounded is omitted from the cached group changes. When that cache is applied, syncCachedChanges advances changesSyncedAt to the cache end; the foreground /v11/changes request then starts after the omitted update, leaving the local blob stale until a later full init. Update the native changes fetcher alongside this JS endpoint so cached iOS windows also include v11 groups.
Useful? React with 👍 / 👎.
…roup-scry-surfaces groups: delete the dead plural ui/groups scries
Summary
Adds an opaque, admin-writable
blob=(unit @t)field to$groupin the %groups agent — a group-level analogue of the channel-level custommetafield — for client-owned group config and bot/automation metadata, plus full frontend plumbing.Fixes TLON-6274
CI will fail until we cut a new pier archives, waiting to do that until its confirmed (or we could PR having the harnesses pull in latest desk changes)
Changes
Backend
blob=(unit @t)on$group;%blobvariants ona-group/c-group/u-group/r-group; new++ v11dependency closure insur/groups-ver/v3/groupslane (group-response-3) emitted first, withgroups-convdowngraders keeping v2/v1/v0 blob-free (same playbook as%active-channel/ 39458f2)group-action-5mark (a-groups:v11);group-action-4stays frozen at v8 so older clients are unaffected%11 → %12(blob defaults to~, pub logs re-walked for embedded%creategroups); groups negotiate version%2 → %3(update/log/command wire shapes change), bumped in%groups,%channels, and%channels-server/x/v3/{groups,changes,groups/…,ui/groups/…},/x/v4/init, andgroups-ui/v9/init+/v10/changes(activity intentionally stays v8 — the unconsumed init-8/activity-v9 bump is not bundled in); new marksgroup-3,groups-3,group-ui-3,group/changed-groups-3,group/response-3,ui/init-9registered in rail/discipline/mark-warmerTypeScript
%blobaction/response;updateGroupBlobpoke viagroupAction5()tracked on/v3/groups; a thirdsubscribeGroupssubscription filtered to blob events (.catchfor older backends)getGroup/getGroupsmoved to the v3 scries; init →/v9/init, changes →/v10/changesgroups.blobdb column (migration regenerated),insertGroupsconflict set,editGroupBlobsync case, optimisticupdateGroupBlobstore actionHow did I test?
./backend/run-tests.shfully green on a fresh pier: 317 OK, Unit tests passed ✅ and Aqua tests passed ✅, including a new+test-c-group-blob(admin set/clear, idempotence, non-admin/non-member nacks)pnpm -r tscclean workspace-wide; api + shared vitest green, including new tests for thetoV1GroupsUpdateblob arm and theeditGroupBlobsync handler%11 → %12migration ran cleanly against real group state; blob set + clear round-trips viagroup-action-5;/v3scries,/v9/init, and/v10/changescarry the blob while/v2surfaces stay blob-freeRisks and impact
Notes: the negotiate bump pauses groups sync between ships until both sides OTA (same as the request-timestamps bump, 027013b). New frontend requires the new desk (
/v3scries and/v9/initare a hard cutover, matching prior init bumps).%createresponses on v1/v2 are blob-stripped; a just-joined group briefly lacks its blob until init/changes/getGroup or the next blob update.Rollback plan
Revert the two commits. The desk state migration is forward-only (
%12), so rolled-back code must not ship to ships that already ran it — roll forward with a fix instead for any ship that has OTA'd.Screenshots / videos
N/A — backend field + API plumbing; no UI changes.
🤖 Generated with Claude Code