Skip to content

feat(chat): create a coalesced inbox record for DM replies (CHE-1793) [squash] - #276

Merged
congvc-dev merged 18 commits into
mainfrom
agent/x99-claude-sonnet/che-1793-n2-dm-notification-record
Oct 17, 2026
Merged

congvc-dev merged 18 commits into
mainfrom
agent/x99-claude-sonnet/che-1793-n2-dm-notification-record

Conversation

@congvc-bot-x99

@congvc-bot-x99 congvc-bot-x99 Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Status

Ready for Cheese's merge decision. This is layer N2 of the DM notification stack (CHE-1789). N1 (PR #272) merged to main as squash commit 6b9b49d803db58ad37386bd2ac3233ab25e02c73, so this PR is based on main. The accepted pair is head 2d2ce28b7f2a5de02070e670f41ecab605cade07, base 2b135399996d8072c5a99deb88ef55edb62f49b4. Merge stays Cheese's gate.

Review: signed independent code PASS from x99-agy-gemini (Google), independent of the Anthropic author, comment 01a14774-b0ab-7a18-9d44-26233b4ed261 on CHE-1793, with a current-pair delegation OCR receipt (24 selected and 4 excluded paths). It covers this pair only.

Checks: exact-head CI run 38987688742 succeeded: 13 successful jobs, 6 skipped. backend-tests and go-lint ran and passed. The PR rollup shows 15 success, 6 skipped, 0 failed, 0 pending. The earlier migration-prefix 627 blocker was repaired on main by PR #290 (CHE-1824). A newer main commit (dbf4adb6e, only server/internal/daemon/execenv files) exists; a trial merge with this branch is clean.

Conditions

  • PR feat(chat): add per-channel notification settings for Inbox and DMs (CHE-1791) #272 (N1) has merged. This PR is based on main. A changed head or base needs fresh checks and independent review with OCR.
  • Changelog: none. The PR that lands the stack on main carries the changelog line.
  • No client renders dm_reply rows yet. A web or mobile row that opens the DM belongs to N4/N5. The InboxItemType union stays closed on purpose, so no label or translation is needed here.
  • Decision pings ignore the dm channel switches. They stay mentioned items and are always delivered.

Change

  • Migrations 650 to 652 (renumbered from 629 to 631, which collided with S1's 629): inbox_item gains chat_session_id, chat_message_id, dedupe_key and a nullable updated_at.
    • Readers use COALESCE(updated_at, created_at). A NOT NULL DEFAULT now() column would re-stamp every old row.
    • A partial unique index on (recipient_id, dedupe_key) WHERE dedupe_key IS NOT NULL AND read = false, and a listing index on the new sort key. Both use CONCURRENTLY in their own files.
    • No foreign keys.
    • The migration-200 index stays, because the archived listing still sorts on created_at.
  • New package server/internal/dmnotify. Reply upserts one unread dm_reply record per (recipient, DM) with dedupe_key = dm:<session id>. A later reply raises details.reply_count, bumps updated_at and un-archives the row. A read record is never reused. The body stays empty: no message text.
  • Reply runs inside the completion transaction (CompleteTask) and the failure transaction (failTask). It creates nothing for an archived DM, a DM with a current channel binding, a decision message kind, or a recipient with both dm switches off. in_app off with push on keeps the record, hidden from the inbox.
  • SQL functions dm_channel_enabled and inbox_item_visible. The inbox list, the archived pages, the unread count and the workspace summary hide a DM record when the recipient can no longer read the DM (membership, session ownership, private-agent access) or when dm in_app is off.
  • Read sync both ways in one transaction: POST /api/chat/sessions/{id}/read reads the DM's dm_reply records. Reading a record, or mark-all-read, advances the DM cursor. Archive and delete read the records. Answering a decision reads its ping record.
  • NotifyPing now sets chat_session_id and chat_message_id, so the decision item opens the DM.
  • Realtime: inbox:new for a new record, new inbox:updated for a coalesced update, both sent only to the recipient. Old clients ignore inbox:updated.
  • API: inbox items carry chat_session_id and updated_at. The zod schema adds both with a null catch, so a wrong type cannot blank the inbox. A row from an older backend still parses.

Evidence

  • Test-first. Commit fe4563c79 changes only tests. Against its parent 48b92ed3e, 9 Go tests (13 with subtests) and 1 vitest test fail on behavior:
--- FAIL: TestDMReplyTenRepliesCoalesceIntoOneUnreadRecord
    dm_reply_notification_test.go:110: dm_reply records = 0, want 1: []
--- FAIL: TestDMReplyReadingTheDMMarksTheRecordRead
    dm_reply_notification_test.go:143: reading the DM must mark its record read: []
--- FAIL: TestDMReplyFailedRunNotifies
    dm_reply_notification_test.go:206: a failed run's error reply must notify: []
--- FAIL: TestDMReplyDecisionRequestMakesOneRecordLinkedToTheDM
    dm_reply_notification_test.go:365: decision record must link the DM ...: {Type:mentioned ... ChatSessionID:<nil> ...}
FAIL  github.com/multica-ai/multica/server/internal/handler
 FAIL  api/schemas.test.ts > InboxItemListSchema > DM reply records (CHE-1793) > normalizes a wrong-typed chat_session_id or updated_at to null
AssertionError: expected { id: 'inbox-1', ... } to match object { chat_session_id: null, updated_at: null }
  • Commits after review: tests-only a93bfd18a (agent rollover regression, red: dm_reply records = 0, want 1 for the new DM, and a failing notification write returned 201 where 500 is expected), then fix 45aeb358d. Review round 3: tests-only ed6ff086d (continue leaves the source DM's unread records, red the archived source DM still has 1 unread dm_reply records for creator and agent; mark-unread of an older read record returned 500), then the fix commit at the PR head: ContinueChatSession reads the source DM's unread records in the continue transaction and publishes inbox:read after commit; MarkInboxUnread returns 409 when a newer unread record already holds the DM. Commit c708c74c5 implements the original change. The same tests pass. Three tests in the same file were added after the implementation and are retrospective: mark-all-read, answering a decision, and the inbox:updated event.
  • Green on an isolated test database, a task-owned pgvector/pgvector:pg17 container on 127.0.0.1:57793.
    • go test ./internal/handler ./internal/service ./cmd/server ./cmd/migrate ./pkg/... all pass.
    • go vet is clean.
    • vitest run in packages/core passes (177 files, 2266 tests).
    • tsc --noEmit in packages/core passes.
  • Earlier text claimed that internal/cli, internal/daemon and internal/daemon/execenv fail the same way on the parent. That claim is withdrawn: the recorded comparison did not show it. In the shell run with MULTICA_TASK_CONFIG_ROOT unset, the parent had 3 execenv Cursor MCP failures. The head had those plus an execenv sidecar failure and a daemon watchdog failure. internal/cli passed on both. Treat the extra failures as environment-sensitive until reproduced. None is claimed as baseline.
  • Migration numbers 650 to 652 are free on main and in every open PR checked on 2026-10-16 (627 to 632 PR feat(wakeups): L9 scoped-event outbox capture and drain (CHE-1199) [squash] #172, 640 to 646 PR feat(wakeups): L10 custom event and condition rules with activation sweep (CHE-1200) [squash] #173, 627 PR feat(chat): deliver held decision groups atomically (CHE-1750) [merge] #260 and feat(chat): archived DMs are unpinned (CHE-1801) #275, 627 and 628 PR feat(chat): add per-channel notification settings for Inbox and DMs (CHE-1791) #272, 629 PR feat(chat): add a DM search endpoint and message anchor (CHE-1792) #273). Combined tree N2 + S1: TestMigrationNumericPrefixesAreUnique failed with prefix 629 shared before the renumber and passes after it.
  • NOT-RUN: end-to-end browser or mobile check, Playwright, lint of the other TS packages, a load test of inbox_item_visible on a large workspace.
  • Not wired on purpose: the three "Stopped." cancel messages and the Mika opening. The DM creator's own action writes each of them, and none is an agent reply. The continuation summary notifies only when the session's chat task wrote it (agent rollover through task_token): ContinueChatSession calls dmnotify.Reply for the new DM inside the continue transaction. A continue the DM creator submits does not notify. The old DM is archived by the same call, so the run's final reply there cannot notify. Design PR docs(chat): design DM search, DM notifications and Web Push (CHE-1790) #271 carries the matching inventory line (head e4e262ce4469938b831b470d29fdac27cee67f5b).

Changelog: none

Refs CHE-1793

congvc-bot[bot] added 15 commits October 16, 2026 02:31
# Conflicts:
#	server/internal/handler/chat.go
@congvc-bot-x99
congvc-bot-x99 Bot marked this pull request as ready for review October 16, 2026 05:41
Base automatically changed from agent/x99-claude-sonnet/che-1791-n1-channel-settings to main October 16, 2026 08:56
@congvc-bot-x99
congvc-bot-x99 Bot marked this pull request as draft October 16, 2026 08:57
congvc-bot[bot] added 2 commits October 16, 2026 15:58
…et/che-1793-n2-dm-notification-record

# Conflicts:
#	server/cmd/migrate/main.go
@congvc-bot-x99 congvc-bot-x99 Bot changed the title feat(chat): create a coalesced inbox record for DM replies (CHE-1793) feat(chat): create a coalesced inbox record for DM replies (CHE-1793) [squash] Oct 17, 2026
@congvc-bot-x99
congvc-bot-x99 Bot marked this pull request as ready for review October 17, 2026 01:49
@congvc-dev
congvc-dev merged commit 6c92e0c into main Oct 17, 2026
22 checks passed
@congvc-dev
congvc-dev deleted the agent/x99-claude-sonnet/che-1793-n2-dm-notification-record branch October 17, 2026 02:19
congvc-bot-x99 Bot pushed a commit that referenced this pull request Oct 17, 2026
Bring in #287 (CHE-1821) and #276 (CHE-1793), merged after the sync base 2b13539.

# Conflicts:
#	server/pkg/db/generated/inbox.sql.go
#	server/pkg/db/queries/inbox.sql
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.

1 participant