Skip to content

fix(app-shell): the chatter reads and writes reactions as the member's own sys_comment_reaction records (objectui#12078) - #12090

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12078-reaction-records
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12078-reaction-records

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12078

Clause-②: yes

What changed

The record chatter (RecordDetailView in @object-ui/app-shell) stores each reaction as the member's own sys_comment_reaction record. It no longer writes the whole sys_comment.reactions column. This follows ruling A, amended, on objectstack-ai/objectstack#22505 (「同意;sys_comment.reactions 可以退役;可以不用考虑历史数据迁移;」) and the director's point 6091621568 (no read-only aggregate).

  • Read. After the comment read, the chatter makes one find on sys_comment_reaction, with comment_id $in the feed's comment ids. It groups the rows client-side into the { emoji: userIds[] } shape and runs them through the same aggregator the column used, so rendering is unchanged. There is no per-comment read. A thread with more than 100 comments is read in pages of 100 ids, in parallel (see the measurement below).
  • Write. A click creates the member's own row, { comment_id, emoji }. The server stamps user_id, so the client sends none. A second click deletes that row by its id. The id comes from the read, or from the create that made the row. Writes for one (comment, emoji) run one after another, so a take-back clicked before its create answers deletes the row that create made. A refused write puts the reaction back and raises the existing detail.reactionFailed error.
  • The cloud window: dual path, no .objectui-sha hold. Where the deployment has no sys_comment_reaction, the column path runs exactly as before, byte for byte. That covers cloud's framework pin 56bf27affb (v17). The chatter asks app-shell's existing useObjectPresence('sys_comment_reaction'); no new probe is written. Following that hook's own contract, only an earned absent keeps the column path. An unsettled registry holds the feed read until it answers. A registry that lists nothing is not read as absence. The file surface is RecordDetailView.tsx and its tests, as declared in the claim.

Measurements (the five mechanism assumptions)

  1. The object (objectstack origin/main 86da1949, plugin-audit/src/objects/sys-comment-reaction.object.ts). Its declared fields:

    • comment_id: text, required, maxLength 255; an id column, not a lookup.
    • emoji: text, required, maxLength 64.
    • user_id: lookup to sys_user, required, stamped from the session on create; a client value is replaced.
    • id and created_at.

    Uniqueness is the index (comment_id, emoji, user_id), unique: 'organization'. A second identical reaction answers 409 UNIQUE_VIOLATION. apiMethods is get, list, create, delete, bulk, with no update.

    • Create: installCommentAccessHooks (authorizeReactionInsert) refuses a reaction on a comment the caller cannot read, and stamps user_id.
    • Delete: the platform's own-record floor (owner_only_deletes, created_by equals the caller). So a member deletes their own reaction and not another member's.
    • Read: a reaction is readable exactly when its comment is.

    The fake server in the new pins applies these same three rules.

  2. The cloud framework 56bf27affb. It has no sys-comment-reaction.object.ts (git cat-file -e exits 128; control sys-comment.object.ts exits 0), and git grep sys_comment_reaction in its plugin-audit/src returns no hits (control: hits on origin/main).

    • Its data door answers an unregistered name with assertObjectRegistered → objectNotFoundError: 404 OBJECT_NOT_FOUND on find and create alike.
    • What the chatter would do there without the gate: data-objectstack's find turns a 404 into { data: [] } and memoises the resource as missing, so every reaction would read as empty. Every click's create would then reject, and the row would roll back with the error. The presence gate keeps the column path there instead.
    • On origin/main the object metadata list prunes nothing per caller (createMetaListReadGate returns the items unchanged for object), so sys_comment_reaction reads present wherever plugin-audit registers it.
  3. The $in read size.

    • The feed's comment read has no page size. It reads the whole thread with no $top, and findData returns the full set when no limit is given.
    • The data door's find is a GET with the filter as JSON in the query string. Measured with Node 22.22.0 (http.maxHeaderSize 16384): each UUID-shaped id costs about 45 bytes, and 100 ids make a 4,588-byte URL. A local Node server accepted 350 ids and answered 431 at 360 ids.
    • So the read is paged at 100 ids (REACTION_READ_COMMENT_IDS), under both Node's limit and an 8 KB proxy request line. A thread of 100 comments or fewer costs exactly one read.
  4. What the user sees.

    • Where the object exists, a reaction stored only in the column is no longer shown. Its chip disappears, as the maintainer's no-migration ruling allows; one pin records this.
    • Each member's click adds or removes only their own reaction, and two members reacting at once both stay.
    • Read from code, not measured live: a member who can read the record but not edit it could not react to someone else's comment before, because the column write is a sys_comment update, which only the author or a parent editor may make. Now they can, because creating a reaction requires only reading the comment.
    • On cloud (object absent) nothing changes.
  5. CommentThread (@object-ui/collaboration) is not on the chatter's path at all. The console chatter renders reactions through plugin-detail's ReactionPicker from FeedItem.reactions. The only app-shell reference to CommentThread is a test. No prop changed.

    • The published surface is untouched. The diff touches only RecordDetailView.tsx, four test files beside it and one changeset, and adds no export line.
    • Clause-②: yes is copied from the claim as asked. The surface reading above is for the in-seat review to judge.

Pins

New file RecordDetailView.reactionRecords-12078.test.tsx. It drives the real RecordDetailView and ReactionPicker over one fake server that several mounted views share, each view signed in as a different member.

  • A member's click on another member's comment creates their own record and nothing else (no user_id sent, no sys_comment update). The feed renders it grouped, and a fresh read shows 👍 2, marked as the clicker's own.
  • A second click removes it. The delete uses the id the create returned, and on a fresh mount the id the read returned.
  • Two members reacting at once both show. Two views are mounted at once, both react before either write answers, and the comment's author then reads 👍 2.
  • One batched read for 3 comments. A 250-comment thread is read in pages of 100, 100 and 50 ids.
  • A reaction stored only in the column is not shown on the records path.
  • A refused create rolls back and raises the error once.
  • A take-back clicked before the create answers deletes the row that create made.
  • The registry decides the store: absent means the column path, with no reaction read and a sys_comment.reactions write; a registry listing nothing means the records path; the comment read waits for a registry that has not answered.

The column-path pins for objectui#11019, objectui#10899 and objectui#11035 now declare a registry without sys_comment_reaction, so they keep pinning the path cloud runs. A header note in each says so.

Ablation (one-time, not kept). On the committed tree (HEAD 086db35), the records branch was switched off and the column-only guard removed, which puts the whole-set column write back on a deployment that has the object.

  • Landing was verified on disk: injected marker count 1, guard count 0.
  • Result: Tests 5 failed | 5 passed (10). The concurrent pin went red with expected [] to deeply equal [ '👍 2' ], and the other four write pins went red with it.
  • Restore was git checkout HEAD --, then a blob hash equal to the HEAD blob eba397e8 and an empty git diff HEAD.

Verification

Every result below is on HEAD 086db35, the last commit. Each heavy step ran through the shared verify lock.

  • Dependency closure build: pnpm --workspace-concurrency=2 --filter "@object-ui/app-shell^..." build, 29 packages. VERDICT command-exit 0.
  • pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0. The test config includes the new test file: its first run, on b886ae3, reported type errors in that file, and they were fixed.
  • pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/views/RecordDetailView, covering every RecordDetailView.* test (the 12 that read sys_comment among them): Test Files 47 passed (47), Tests 294 passed (294).
  • eslint on the six touched files: 0 errors. The warnings on added lines are no-explicit-any in the new test, the same pattern as its sibling tests. Two more sit on lines this diff edited but did not cause: the existing res: any, and the existing missing-t dependency note.
  • Static checks, all exit 0: check:new-line-citations (0 new), check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check-changeset-presence.mjs and check-changeset-no-major.mjs. check-governed-queue-guard.mjs --test answers NOT GOVERNED for the six paths.
  • Not owed: the i18n checks. No locale string changed; the error reuses detail.reactionFailed.
  • NOT MEASURED: check:eager-closure, because it needs a full console production build. CI's Bundle Analysis runs it on this pull request. No module enters the first-load closure: useObjectPresence is already reached through the header's sharedUserFeeds. Only the bytes of the chunk that holds RecordDetailView grow.

Acceptance notes

  • The column branch is transitional. It can be deleted, with its three column pins, once cloud's framework carries sys_comment_reaction; Retire sys_comment.reactions (ruling A amended on #22505): no aggregate, no data migration, after the console reads reaction records objectstack#22573 retires the column itself. No card is filed for this.
  • If the registry ever settles as error on a v17 server, the records path runs there: reactions read empty and a click is refused with the error. This is the useObjectPresence contract (only an earned absent changes the path). It is a narrow window, and the console's nav and object views need the same registry anyway.
  • A stale screen: when a member has already reacted in another tab and clicks the same emoji here, the create answers 409 UNIQUE_VIOLATION. The chip rolls back with the error until the next read shows the stored reaction. Nothing is overwritten.
  • The feed's sys_comment read itself has no page size. That is unchanged here and only observed.

Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z


Generated by Claude Code

…s own sys_comment_reaction records

A reaction click wrote the comment's whole sys_comment.reactions set back
with one update, so two members reacting at once left only the later write.
Where the deployment has sys_comment_reaction, the chatter now reads the
comments' reaction rows in one batched comment_id $in read (paged at 100
ids), groups them into the shape the panel renders, and a click creates or
deletes the clicker's own row. Where the object registry earns "absent"
(a framework that predates the object), the column path is kept unchanged.

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3169.4 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-9tfe154l.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 587.83KB 141.44KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 086db35b0226fd217a85901d2405eac2d0174763
Local-runs: none

① Derived judgments

Inputs: card objectui#12078 (body and all comments); PR #12090 (body, file list, and the net diff against origin/main at merge-base 023f00d4); git show of the head file and of origin/main for useObjectPresence, the @object-ui/collaboration entry, the data-objectstack adapter and @object-ui/core's filter converter; and the head's check-runs. Nothing was built, run or re-run.

  1. Public surface of @object-ui/app-shell: unchanged — RIGHT.
    • The diff adds no export, and packages/app-shell/src/index.ts is not in the file list.
    • RecordDetailViewProps is unchanged.
    • Every new symbol is module-private: COMMENT_REACTION_OBJECT, REACTION_READ_COMMENT_IDS, readCommentReactionRows, groupReactionRows, holdsOwnReaction and ReactionRecordQueue.
    • The one import added from @object-ui/types (DataSource) is type-only.
  2. @object-ui/collaboration and @object-ui/types: untouched — RIGHT. The claim's Clause-② reason did not materialise.
    • On origin/main, CommentThread and CommentThreadProps are exactly as before.
    • The console chatter renders reactions through plugin-detail's ReactionPicker, from FeedItem.reactions.
  3. Language-pack keys: none added or removed — RIGHT. The records path raises detail.reactionFailed, which every pack already carries.
  4. Read behaviour on the wire — RIGHT against the card.
    • After the sys_comment read comes one find on sys_comment_reaction, with comment_id $in the feed's comment ids, in pages of 100 ids run in parallel. It never reads per comment.
    • The rows are grouped client-side into the emoji to userIds[] shape and passed through the same toReactions aggregator, so rendering is unchanged.
    • That meets the card's read rule and the director's no read-only aggregate point.
    • The $in reaches the wire: convertFiltersToAST maps $in to in.
    • Paging is the card's own allowance (「Page it if needed」), and the bound is measured in the PR.
  5. Write behaviour on the wire — RIGHT against the ruling.
    • A click sends create('sys_comment_reaction', { comment_id, emoji }), with user_id stamped by the server, or delete('sys_comment_reaction', rowId). The records path never updates sys_comment.
    • That is the maintainer's amended ruling A (「同意;sys_comment.reactions 可以退役;可以不用考虑历史数据迁移;」).
    • The adapter contract holds on origin/main: create resolves the record, and delete resolves the boolean success.
    • Writes for each (comment, emoji) pair are serialised. The settle step compares the server's state with the newest click's wish and rolls back once, with the existing toast.
  6. Cloud window — RIGHT, by the first of the card's two allowed shapes.
    • The chatter asks the existing useObjectPresence('sys_comment_reaction'), and an earned absent selects the column path, which the diff leaves byte-identical.
    • The three column pins (11019, 10899, 11035) now declare a registry without the object, so they keep pinning the path cloud's v17 pin 56bf27affb runs.
    • No .objectui-sha hold is needed.
    • The feed read waits while the registry is unsettled. That is the hook's documented contract, and with no provider the hook reads as settled.
  7. The authorisation widening is named, not hidden — RIGHT. A member who can read a record but not edit it can now react on another member's comment, because a reaction create is objectstack's own object rule (the reactor's own record). That is consistent with the ruling and is not an accept-set of this repo.
  8. Pins — RIGHT.
    • All four card pins have a test, over one fake server shared by several views signed in as different members.
    • The ablation (the column write put back) is reported red on the concurrent pin, and the restore was proved by blob hash.

② Semver level

  • '@object-ui/app-shell': patch with no major. The changeset gates are green on the head.
  • patch matches what the diff publishes: no export, prop, type member or language-pack key moves. The only user-visible loss is the ruled no-migration.
  • Clause-②: yes was declared. In substance the diff widens neither an accept set nor the public surface. yes is the conservative direction, and this record carries the finding; no re-declaration is owed.

③ Boundary flags

Dev deviations (the os-dev-report on the card):

  1. The PR body says "six touched files" for eslint, which ran on the five .tsx files. This is cosmetic: Lint is green on the head.
  2. The commit trailers and footer follow objectui AGENTS.md's model-free form. Not a deviation in substance.
  3. check:eager-closure was not run locally. Closed by the head's green Bundle Analysis.
  4. CommentThread.tsx is untouched. Correct, per ① item 2.

open_questions: none declared, none found.

out_of_scope_findings:

  1. The column branch and its pins can be deleted once cloud's framework carries the object. The carrier is Retire sys_comment.reactions (ruling A amended on #22505): no aggregate, no data migration, after the console reads reaction records objectstack#22573 or the cloud v18 pin bump.
  2. The feed's sys_comment read has no $top. Pre-existing and unchanged by this diff.
  3. A member who already reacted in another tab gets a 409, and the chip rolls back with the toast until the next read. Nothing is overwritten, and the error is surfaced.

Reviewer flags, none blocking:

  • The error-settled registry window on a v17 server: the records path, empty reads, and refused writes with the toast. It is narrow, it follows the hook's contract, and the cloud v18 pin bump closes it.
  • A swallowed reaction-read failure keeps the previous read's own-row ids, so a take-back can be refused and rolled back with the toast. Self-correcting on the next read.

Check-runs on the head, read after every run had completed: 43 check-runs, 40 success, 3 skipped by design (Test (coverage), the unexpanded coverage matrix placeholder and dependabot), 0 failed, 0 pending. Interim readings taken while shards were still running were not posted.

Implemented-by: claude/issue-12078-reaction-records
Reviewed-by: session_01B1gHb9baeX7oioD5sHVm7z

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 10:07
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 10:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit de302c7 Oct 10, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12078-reaction-records branch October 10, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants