Inbox Digests And Threaded Notification Entries

Summary

AgentInbox should reduce bursty notification noise without hiding or destroying the original source events.

The current activation buffer can coalesce bursts into fewer dispatches, but it still leaves the runtime with the same core problem:

This RFC proposes a two-layer model:

In this model:

Problem

For bursty sources such as GitHub PR reviews or CI updates, one logical unit of work often arrives as many raw events:

Today AgentInbox already batches activation dispatches using a service-level window and max-items threshold, but that only reduces activation frequency. It does not change the agent-facing read surface.

That means the runtime still has to:

  1. receive an activation
  2. read raw inbox items
  3. discover that many items are about the same object
  4. re-aggregate them in runtime logic

This pushes grouping semantics back into the runtime, which conflicts with the product boundary.

Goals

Non-Goals

Design Principles

1. Raw Facts Stay Raw

InboxItem remains the immutable, durable record of what the source produced.

Aggregation should not mutate or collapse the fact layer. It should build an agent-facing read model on top of it.

2. Read Units Must Be Stable

If a runtime reads a digest and later acks it, the meaning of that ack must not change underneath it.

That means:

3. Grouping Belongs At The Inbox Presentation Boundary

Grouping should happen:

It should not live:

4. Source Semantics At The Edge

The core should not try to infer too much provider-specific meaning.

Source or module implementations should be able to supply:

The core can provide a conservative fallback, but high-quality grouping should come from the source edge.

Current State

Today the relevant pieces are:

The existing activation buffer:

So it solves “fewer wakeups” but not “fewer things to read”.

Core Design

1. Add An Agent-Facing InboxEntry Read Model

InboxItem remains the fact layer.

On top of it, AgentInbox should introduce a new agent-facing read model:

An InboxEntry is what runtimes read, what activations reference, and what ack targets by default.

Suggested entry kinds:

This means the runtime-facing mailbox becomes a presentation model, not just a raw event table.

2. Introduce DigestThread As The Logical Group

Some related events should continue to accumulate into one logical thread as long as that thread remains active and unacked.

Suggested internal object:

Responsibilities:

DigestThread is mutable and long-lived.

It is not the object that runtimes ack directly.

3. Materialize Immutable DigestSnapshot Entries

Because a live thread can change over time, runtimes should never ack the live thread directly.

Instead, each visible grouped entry should be an immutable snapshot of a thread at a particular revision.

Suggested object:

Suggested fields:

Behavior:

This is what keeps read/ack semantics stable.

4. Grouping Is Keyed By Resource + Event Family

Grouping should be driven by a source/module-provided grouping hint.

Suggested minimum group key components:

Examples:

Default fallback should be conservative, for example:

If a source cannot provide useful grouping semantics, AgentInbox should prefer under-grouping over incorrect grouping.

5. Aggregation Policy Belongs To The Agent-Facing Inbox Layer

Because grouped entries are part of the shared read model for an agent inbox, aggregation policy should not be target-specific.

That is an important boundary:

Suggested placement:

Phase 1 suggested policy shape:

type InboxAggregationPolicy = {
  enabled?: boolean;
  windowMs?: number;
  maxItems?: number;
  maxThreadAgeMs?: number;
};

This is separate from:

6. Two-Stage Aggregation: Burst Buffer + Unacked Thread Merge

Pure time-window grouping is not enough.

If grouping only happens within a short flush window, it solves simultaneous bursts but not long-lived notification threads like social-network notifications.

So the model should have two stages:

Stage A: Burst Buffer

New raw items enter a short pending aggregation buffer keyed by the grouping key.

This prevents re-materializing the thread on every single event in a burst.

Stage B: Unacked Thread Merge

When the burst buffer flushes:

This is what allows cross-time grouping while still keeping stable snapshots.

7. Flush Rules

The pending burst buffer should flush when any of these happen:

The logical thread itself should roll over and start a new thread when:

8. Source/Module Hooks

RemoteSourceModule should be able to provide optional grouping hooks.

Suggested hook shape:

deriveNotificationGrouping?(item, source, subscription) => {
  groupable: boolean;
  resourceRef?: string | null;
  eventFamily?: string | null;
  summaryHint?: string | null;
  flushClass?: "normal" | "immediate";
}

Later, modules may also provide:

summarizeDigestThread?(items, context) => string | null

This keeps source semantics at the edge:

9. Identifier Strategy

The new aggregation-layer objects should optimize for local operator and runtime interaction, not for globally opaque UUID-style identifiers.

For this RFC, the recommended strategy is:

Suggested agent-facing refs:

Examples:

This keeps the storage model simple and the agent-facing surface compact:

The database should still keep ordering concerns separate from identity.

Recommended shape:

Where:

If phase 1 wants to keep things even simpler, it is acceptable for entry_id == sequence, but the conceptual model should still treat:

Formatting and parsing the typed refs should happen at the boundary layer. The database itself does not need to store literal strings like ent_42.

Read And Ack Semantics

1. Read Returns Stable Entries

Default inbox read should return InboxEntry[], not just raw inbox items.

For grouped bursts, the runtime sees one digest_snapshot entry instead of many related raw items.

If the runtime wants details, it can explicitly expand a digest snapshot into its referenced raw itemIds.

2. Ack Targets Immutable Snapshots

Ack should continue to support batch semantics, but the ack object should be a stable entry boundary.

Recommended behavior:

For digest snapshots:

This preserves stable boundaries even when the underlying digest thread keeps growing.

3. Superseded Snapshots Stay Valid Ack Boundaries

If a digest thread receives more items and materializes a newer snapshot:

This avoids the “read/ack window changed underneath me” problem.

Activation Semantics

Activation should target InboxEntry units, not raw inbox item batches.

That means:

This keeps activation, read, and ack aligned on the same agent-facing object.

Compatibility

Phase 1 does not need to remove the raw-item APIs immediately.

A safe rollout path is:

  1. keep raw InboxItem storage and internal behavior unchanged
  2. add InboxEntry and digest thread materialization alongside it
  3. make runtime-facing inbox read default to entries
  4. keep an explicit raw-item read path for debugging, replay, and low-level tooling

This preserves operability while shifting the default experience to grouped reads.

Observability

Aggregation must be observable.

Suggested metrics/events:

Without these, grouping bugs will be difficult to debug.

Why Not Only Activation-Level Aggregation

An earlier, simpler option would have been:

This is insufficient because it only reduces wakeups, not runtime work.

The runtime would still need to:

That would move grouping semantics back into runtime logic, which is not the intended boundary.

Why Not Replace Raw Items Entirely

Replacing raw items with only aggregate records would make:

So raw InboxItem records must remain the durable fact layer.

Phased Rollout

Phase 1

Phase 2

Phase 3

Open Questions

Conclusion

AgentInbox should not stop at reducing activation frequency.

To actually lower runtime overhead, it needs an agent-facing grouped read model that:

That gives AgentInbox a real inbox-level notification aggregation model, instead of leaving runtimes to rediscover grouping from raw events.