Build a Community Digest That Shows What AI Did—and Did Not—Summarize
Community opportunity channels create an uncomfortable trade-off: read every message or trust a summary that may omit the one deadline you needed. An AI-generated digest can reduce reading time, but it cannot remove that uncertainty by itself. A model may shorten text reasonably well, yet still ski

Community opportunity channels create an uncomfortable trade-off: read every message or trust a summary that may omit the one deadline you needed.
An AI-generated digest can reduce reading time, but it cannot remove that uncertainty by itself. A model may shorten text reasonably well, yet still skip a qualification, alter a date, or give disproportionate attention to one post. The human concern underneath the AI discussion is not simply accuracy. It is loss of control over what gets noticed.
The safer design is to keep selection deterministic and use AI only for presentation:
This tutorial builds that control layer in TypeScript, then shows where it connects to a Tencent RTC Social Messaging community.
Our digest will follow one central invariant:
That is stronger than asking an LLM to “summarize everything important.” Importance is subjective, while inclusion can be tested.
We will also separate four decisions that are often accidentally bundled together:
AI is useful here as a compression tool. It is not demonstrated to be a reliable authority for deadlines, eligibility, safety, or editorial priority.
Timestamps are not sufficient for a reliable cursor. Two messages can share a timestamp, clocks can differ, and late ingestion can put an older timestamp after the checkpoint.
Assign each ingested community message a monotonically increasing application sequence:
101 Message A 102 Message B 103 Message C ^ frozen ceiling Enter fullscreen mode Exit fullscreen mode A digest records both its starting checkpoint and its frozen ceiling. New messages receive higher sequence values and belong to the next run.
The minimum source record looks like this:
Use a current Node.js release with TypeScript support:
mkdir controlled-community-digest cd controlled-community-digest npm init -y npm install --save-dev typescript tsx @types/node npm pkg set type = module npm pkg set scripts.test = "tsx --test src/digest.test.ts" mkdir src Enter fullscreen mode Exit fullscreen mode Create tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noUncheckedIndexedAccess": true, "outDir": "dist" }, "include": [ "src" ] } Enter fullscreen mode Exit fullscreen mode Represent omissions as states, not missing array entries Create src/digest.ts:
In production, consider bounded concurrency rather than the serial loop shown here. Preserve the frozen manifest and item order regardless of how many model calls run concurrently.
A post may be edited, deleted, or withdrawn while the AI is working. Approval must compare the draft with current source versions.
A single “Approve digest” button can conceal unread AI output. Require the reviewer to acknowledge every AI-authored item:
export function approveDigest ( input: { digest: Digest; reviewerId: string; reviewedAiMessageIds: Set < string >; }): Digest { const { digest, reviewerId, reviewedAiMessageIds } = input; if ( digest. state!== " awaiting_review " ) { throw new Error ( `Cannot approve a digest in state ${ digest. state } ` ); } const unresolved = digest. items. filter ( ( item ) => item. state === " changed_after_collection " ); if ( unresolved. length > 0 ) { throw new Error ( " Resolve changed source messages before approval. " ); } const unreviewedAiItems = digest. items. filter ( ( item ) => item. state === " ai_draft " &&! reviewedAiMessageIds. has ( item. messageId ) ); if ( unreviewedAiItems. length > 0 ) { throw new Error ( " Every AI draft must be acknowledged by the reviewer. " ); } digest. reviewerId = reviewerId; digest. state = " approved "; return digest; } Enter fullscreen mode Exit fullscreen mode The review interface should place the original and draft side by side.
Highlight dates, URLs, monetary amounts, eligibility language, and proper names for human comparison. Those highlights can be deterministic; they do not need another model.
A network timeout does not prove that publication failed. Blindly retrying can produce duplicate digests.
export type PublishOutcome = | { kind: " accepted "; receiptId: string } | { kind: " rejected "; reason: string } | { kind: " unknown "; reason: string }; export interface DigestPublisher { publish ( input: { communityId: string; digestId: string; body: string; }): Promise < PublishOutcome >; } export interface CheckpointStore { commit ( input: { digestId: string; communityId: string; expectedAfter: number; through: number; }): Promise < void >; } export function renderDigest ( digest: Digest ): string { const lines = digest. items. map (( item, index ) => { const heading = ` ${ index + 1 }. Source message ${ item. messageId } `; switch ( item. state ) { case " ai_draft ": return ` ${ heading } \nAI-assisted summary: ${ item. summary } `; case " verbatim_fallback ": return ` ${ heading } \nOriginal excerpt: ${ item. originalText } `; case " unsupported_content ": return ` ${ heading } \nNot summarized: unsupported content type.`; case " withdrawn ": return ` ${ heading } \nWithdrawn before publication.`; case " changed_after_collection ": throw new Error ( " Changed items cannot be rendered for publication. " ); } }); return [ " Community resource digest ", " AI-assisted text is labeled.
Verify details against the source message. ", "",... lines ]. join ( " \n " ); } export async function publishDigest ( input: { digest: Digest; publisher: DigestPublisher; checkpoints: CheckpointStore; }): Promise < Digest > { const { digest, publisher, checkpoints } = input; if ( digest. state!== " approved " && digest. state!== " checkpoint_pending " ) { throw new Error ( `Cannot publish a digest in state ${ digest. state } ` ); } if ( digest. state === " approved " ) { digest. state = " sending "; const outcome = await publisher. publish ({ communityId: digest. communityId, digestId: digest. id, body: renderDigest ( digest ) }); if ( outcome. kind === " unknown " ) { digest. state = " delivery_unknown "; digest. lastError = outcome. reason; return digest; } if ( outcome. kind === " rejected " ) { digest. state = " approved "; digest. lastError = outcome. reason; return digest; } digest. receiptId = outcome. receiptId; digest. state = " checkpoint_pending "; } try { await checkpoints. commit ({ digestId: digest. id, communityId: digest. communityId, expectedAfter: digest. afterSequence, through: digest. throughSequence }); digest. state = " published "; digest. lastError = undefined; } catch ( error ) { digest. state = " checkpoint_pending "; digest. lastError = error instanceof Error? error. message: " Checkpoint commit failed "; } return digest; } Enter fullscreen mode Exit fullscreen mode There are two important recovery paths:
The production checkpoint operation should be conditional on expectedAfter and idempotent by digestId. That prevents two scheduler instances from advancing the same community cursor independently.
npm test Enter fullscreen mode Exit fullscreen mode These tests verify the control boundary, not an LLM's prose quality. Summary quality needs an evaluation set containing the kinds of community posts you actually publish, especially posts with dates, restrictions, corrections, and multiple links.
Tencent RTC's Social Messaging solution covers group discussions and community experiences, which makes the community conversation the appropriate user-facing surface for this workflow:
https://trtc.io/solutions/social-messaging
Keep the integration behind adapters rather than inventing product methods:
A multilingual community may also offer on-demand translation. Tencent RTC documents TUIChat message translation here:
The documentation includes supported content types, languages, and edition limitations, so verify those conditions for your deployment rather than assuming every digest can be translated.
Translation should remain a display operation:
If a translated deadline conflicts with the original, the original source remains authoritative and should be escalated to a person.
This table is also a useful product requirement. If a state has no understandable interface treatment, it is probably not ready for production.
Run these checks with a staging community:
The durable engineering skill here is not writing a more persuasive summarization prompt. It is deciding which parts of a community workflow may be probabilistic.
For your own community, the useful design question is: Which omission would be most costly, and can your manifest prove that it did not happen?
Disclosure: This article was created in collaboration with Tencent RTC. Official Tencent RTC documentation was used as the implementation reference.