Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
253c488
Added Native Streams high-level and detailed design docs.
moedash Aug 23, 2026
37a5a22
Compared Native Streams against the two existing prototypes.
moedash Aug 23, 2026
6c6d823
Proved the history-node store works as a general append-only log.
moedash Aug 23, 2026
20f15ed
Dropped underscores from the stream-log test names for the linter.
moedash Aug 23, 2026
ee381c6
Folded Option 7 into the comparison and deferred to the Notion doc.
moedash Aug 23, 2026
60fc894
Took the partition fix and run-transition handling from the Codex des…
moedash Aug 23, 2026
d61adc4
Measured what Workflow Streams actually costs today.
moedash Aug 24, 2026
eb88450
Added the stream CHASM component and its bucketed log.
moedash Aug 24, 2026
b67261b
Defined the StreamService API surface.
moedash Aug 24, 2026
95fbc42
Wired StreamService through history and frontend.
moedash Aug 24, 2026
ec0bdf7
Added end-to-end stream tests and fixed three bugs they found.
moedash Aug 24, 2026
4d26293
Added long-poll tailing and a shard-local tail cache.
moedash Aug 24, 2026
3581563
Added cap-driven truncation, bucket reclamation, and retention.
moedash Aug 24, 2026
f29c14f
Made streams listable.
moedash Aug 24, 2026
c94a2d1
Attributed stream persistence calls to their namespace.
moedash Aug 24, 2026
e49edb3
Let callers supply a stream's run id, and measure the difference.
moedash Aug 24, 2026
6f5a6e6
Measured native streams against the pattern they replace.
moedash Aug 24, 2026
aaaa1ec
Specified the api-go changes Paths A and C need, and why they are blo…
moedash Aug 24, 2026
3bb8f73
Added a runnable native-streams demo.
moedash Aug 24, 2026
036606f
Pinned the server to an api fork carrying the stream shapes.
moedash Aug 24, 2026
b124727
Rebuilt the API fork through the project's own pipeline.
moedash Aug 24, 2026
e301a22
Added Path A: a workflow publishing to a stream it owns.
moedash Aug 25, 2026
7c074ef
Resolved how Path C replay obtains stream bytes.
moedash Aug 25, 2026
9279de8
Corrected the completed-event field name in sequencing.
moedash Aug 25, 2026
4046164
Narrowed the in-workflow caveat to measurement coverage.
moedash Aug 25, 2026
4b792b0
Added a guard for the started-response wire-compatible pairs.
moedash Aug 25, 2026
2463dd7
Threaded stream slices through the workflow task response.
moedash Aug 25, 2026
2c7c102
Added the consumer cursor a workflow keeps for a stream.
moedash Aug 25, 2026
c99e812
Recorded where the consumer cursor lives and why.
moedash Aug 25, 2026
eb999b2
Wired stream delivery and cursor recording into workflow tasks.
moedash Aug 25, 2026
543bf8b
Delivered stream slices to consuming workflow tasks.
moedash Aug 25, 2026
5e6e158
Pinned a stream's floor to its in-workflow consumers.
moedash Aug 26, 2026
4c86fa8
Woke a subscribed workflow when its stream runs ahead.
moedash Aug 26, 2026
232c886
Re-supplied consumed ranges to a replaying worker.
moedash Aug 26, 2026
58fa960
Let a workflow consume a stream in another execution.
moedash Aug 26, 2026
2998caf
Read a stream's log from the shard that holds it.
moedash Aug 26, 2026
b8ff6db
Carried stream subscriptions across continue-as-new.
moedash Aug 27, 2026
65f187f
Let a workflow subscribe to a stream with a command.
moedash Aug 27, 2026
935fbf3
Gave the subscribe command a history event.
moedash Aug 27, 2026
ec69563
Aligned the stream imports with the repo's alias rules.
moedash Aug 28, 2026
5a4fe2d
Recorded a stream publish as a history event.
moedash Aug 28, 2026
a497d26
Measured what a history event per publish costs.
moedash Aug 28, 2026
f318f7a
Bounded a poll's read and keyed the tail cache by log.
moedash Aug 28, 2026
e36d24f
Bounded the stream's unbounded per-host and per-stream tables.
moedash Aug 28, 2026
606d6a9
Separated a stream append's txn id by workflow task attempt.
moedash Aug 28, 2026
6c58e6b
Delivered stream ranges on the inline workflow task path.
moedash Aug 28, 2026
c5469f5
Stopped re-supplying a consumed range to a sticky task.
moedash Aug 28, 2026
c57ed7e
Gave a repeated subscribe its history event.
moedash Aug 28, 2026
628d509
Judged the payload-leak check against a leak, not against zero.
moedash Aug 28, 2026
15941b5
Added a harness comparing bucket 2 against the Redis prototype.
moedash Aug 30, 2026
b65df35
Let a reader outside a workflow read the stream it publishes.
moedash Sep 1, 2026
b914746
Let a producer outside a workflow append to the stream it owns.
moedash Sep 1, 2026
2b82747
Ended a reader when the workflow it reads from ends.
moedash Sep 1, 2026
0e74266
Numbered every stream write from one sequence.
moedash Sep 2, 2026
8363ddf
Kept the scavenger off stream logs, and the subscribe event in order.
moedash Sep 2, 2026
fa6b439
Made a lost consumer notification retry instead of vanishing.
moedash Sep 2, 2026
56b4398
Routed the two cross-execution steps that could be routed.
moedash Sep 2, 2026
b56177e
Measured Cassandra partitioning before committing to a substrate.
moedash Sep 2, 2026
c272bbb
Moved the stream log onto a table of its own.
moedash Sep 2, 2026
61f87af
Removed the transaction id the new substrate does not need.
moedash Sep 2, 2026
c5c08a3
Stopped a consumer holding the stream's floor.
moedash Sep 2, 2026
343d19d
Designed stream log replication, which the substrate change made cheap.
moedash Sep 2, 2026
b3c6d63
Worked out which rows a replicated stream has to send.
moedash Sep 2, 2026
6418bbf
Turned the replication note into a CHASM framework request.
moedash Sep 2, 2026
5596837
Fixed the drain order and stopped reporting an absence as a zero.
moedash Sep 2, 2026
feab9aa
Made the benchmark latency sink count each token once.
moedash Sep 2, 2026
b9abd71
Recorded the answer on where a stream's bytes belong.
moedash Sep 2, 2026
b86fbab
Re-ran the Option 5 against Option 7 head to head.
moedash Sep 2, 2026
fc4d68b
Moved the stream payload onto the component as a chasm.Map.
moedash Sep 2, 2026
fde8a7b
Removed the tail cache, which no longer has a read to save.
moedash Sep 2, 2026
8ed6d78
Read Path C deliveries from the component holding them.
moedash Sep 2, 2026
e056229
Re-measured both benchmarks against the component-held payload.
moedash Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude/skills/blame/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
description: Show which re_gent step last modified each line of a file. Use when investigating file provenance or debugging.
allowed-tools: Bash(rgt blame *)
argument-hint: "<path>[:<line>]"
---

Display per-line provenance.

Run:
```bash
rgt blame $ARGUMENTS
```
23 changes: 23 additions & 0 deletions .claude/skills/log/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
description: View the re_gent activity log for the default or selected session. The default view shows the conversation timeline and tool calls; file summaries are available with file flags.
allowed-tools: Bash(rgt log *)
argument-hint: "[session-id] [flags]"
---

Display the re_gent activity log.

By default, `rgt log` shows the conversation timeline for the most recent session with captured steps. Use `--files-only` for file-change summaries.

Run:
```bash
rgt log $ARGUMENTS
```

Common usage:
```bash
rgt log
rgt log --conversation-only
rgt log --files-only
rgt log --graph
rgt log --limit 50
```
12 changes: 12 additions & 0 deletions .claude/skills/show/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
description: Show detailed context for a re_gent step, including tool calls, tool results, and conversation.
allowed-tools: Bash(rgt show *)
argument-hint: "<step-hash>"
---

Display full details for a step.

Run:
```bash
rgt show $ARGUMENTS
```
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,9 @@

# Ignoring AI agent files
.agents/

# Compiled output of develop/streamdemo.
/streamdemo

# Review scaffolding, kept out of the repo
/prompt.txt
788 changes: 407 additions & 381 deletions api/historyservice/v1/request_response.pb.go

Large diffs are not rendered by default.

684 changes: 356 additions & 328 deletions api/matchingservice/v1/request_response.pb.go

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions chasm-external-data-request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Where a stream's bytes live: asked, and answered

Status: **answered 2026-09-02.** This started as a request to CHASM owners for a
node kind covering data a component owns but does not store inline. The answer
is that it is already planned, so the request is withdrawn and this file is the
record of the answer and what it means for the prototype.

## The answer

Yichao Yang: the underlying event storage in CHASM is planned to be exposed in
**H2, with replication and lifecycle**. Roey Berman confirmed that streaming was
expected to use it. So the general capability is coming, and nothing in this
prototype should be built to substitute for it.

That closes the question this file originally asked. Not by argument, but
because the work is scheduled and owned elsewhere.

## Why the question came up

A stream's messages cannot live in mutable state. All four CHASM node kinds
store their bytes inline (`chasm.proto:27-32`) in
`WorkflowMutableState.chasm_nodes` (`workflow_mutable_state.proto:19`), and:

- `chasmNodeSizes` (`mutable_state_impl.go:170`) feeds `approximateSize`
- checked against `MutableStateSizeLimitError`, 8 MB, warn at 1 MB
(`dynamicconfig/constants.go:473-481`)
- over the error limit the execution is **force-terminated**
(`context.go:1381`, `maxMutableStateSizeExceeded`)
- archetype-tagged, so a standalone stream entity is subject to it exactly as a
workflow is

Measured: a 100k-token stream is 5.84 MB of payload. Inline, that is one long
agent conversation past the warn and approaching the limit, and every append
rewrites the whole record on the way there.

## Prior art, which reached the same ceiling

Dan Davison and Sean Kane built a CHASM streaming component about a year ago,
on `seandan/streaming` in `temporalio/temporal`. It is roughly 1500 lines under
the same `chasm/lib/stream/` path this prototype uses, with `AddToStream` and
`PollStream` on the frontend and one end-to-end test. Its component is:

```go
type Stream struct {
chasm.UnimplementedComponent
*streampb.StreamState // head, tail
Messages chasm.Map[int64, *commonpb.Payload] // one data node per message
}
```

Roey Berman flagged its ceiling at the time: limited to about 5 MB of total
payload because everything is in mutable state. The same note enumerated three
ways out, and they are the same three this prototype arrived at independently:
CHASM nodes in separate cells, payloads written outside mutable state by
repurposing history, or payloads replaced by pointers to a blob store.

Worth being plain about it: this prototype's first substrate was the second of
those, and the request this file used to make was the third. Neither was a new
idea. Finding that out after the fact is the cost of not having read
`#crew-streaming` first.

## The one thing that is not settled

Dan Davison's guidance is to not make the storage realistic: use something based
on `chasm.Map` and assume it has the properties it needs, because the value of
this prototype is exploring user-facing behaviour that other design sessions
might miss. That is a coherent position and it is what the prior art does.

It does not carry the benchmark, which is the other half of AI-198. The point of
measuring client-side streaming against server-side streaming is to find out
what each costs, and a substrate that force-terminates the entity partway
through the workload cannot produce a number anyone should quote. The two goals
want different things from the same prototype, and that is the disagreement to
resolve rather than paper over.

Separately, Paul Nordstrom has asked for a discussion before this goes further,
on the grounds that the data team owns backend storage for the stream affordance
and this is not a place for a one-off, and that the short-term path agreed with
Max was a client-side Redis connection. That is an ownership question, not a
technical one, and it is not mine to settle here.

## What the prototype assumes in the meantime

An external shared store, with `stream_log` as the working implementation. Not
because it should ship, but because the benchmark needs a substrate that does
not fall over inside the workload. When CHASM event storage lands, this is the
piece that gets deleted.

The consequence already recorded: the three persistence methods sit on
`ExecutionStore` (`persistence_interface.go:168-175`), which presumes the log is
shard-local. Under any external store that is the wrong home. Given the answer
above, moving it is probably wasted work, so it stays as it is.

## What replication does, until then

Nothing. A standby holds a frontier and no bytes, so a failover breaks every
reader. That is now a documented limitation of a prototype rather than a gap to
close, because the replication of stream bytes arrives with CHASM event storage
in H2.

The protocol sketch that used to be the point of this file is kept below,
because it is cheap to keep and it is a concrete answer to one question that
CHASM event storage will have to answer too: how a receiver tells a sender which
byte ranges it already holds.

Three properties, all from keying a record by the offset it starts at. Applying
a record is idempotent, so duplicates and retries are free. Records are
independent, so out-of-order arrival is harmless. A record is self-describing,
so a receiver needs no context to place it. Together they make shipping the
payload viable rather than fetching it back the way history events are.

Carry the records in the message that already carries the frontier, and apply
records before state. Ordering is then free, which matters because it is the
thing most likely to be got wrong: shipped separately, the frontier can arrive
first and the standby holds offsets whose bytes never landed.

For which records to ship, have the receiver report the offset it holds through,
per collection, and ship from there to the frontier. It needs a back-channel
that does not exist, since receiver progress travels today as a per-shard
task-id acknowledgement. It pays for itself by making the byte cap free: a
message that cannot carry the whole range carries a prefix, and the receiver's
next report resumes exactly there, with no continuation token and no resumption
state.
53 changes: 53 additions & 0 deletions chasm/lib/stream/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package stream

import "time"

// DefaultMaxMessagesPerPoll bounds a read page when the caller does not.
const DefaultMaxMessagesPerPoll = 1000

// MaxMessagesPerBatch bounds one append. It is not only an admission limit: a
// node ID is the first offset of its batch, so to serve a read starting inside
// a batch the reader has to find the node that contains it. Bounding the batch
// bounds how far back it has to start, which turns an unbounded scan into a
// fixed overread.
const MaxMessagesPerBatch = 1000

// LongPollTimeout matches the convention used by the history long polls: on
// expiry the caller gets an empty response and polls again, rather than an
// error it would have to special-case.
const LongPollTimeout = 20 * time.Second

// LongPollBuffer leaves room to return an empty response before the caller's
// own deadline fires.
const LongPollBuffer = 3 * time.Second

// Tail-cache bounds. Sized for many modest streams rather than a few large
// ones, which is the shape this primitive targets.
const ()

// MaxConsumeItemsPerTask bounds one Workflow Task's slice. A byte cap alone is
// not enough: a burst of tiny messages stays under it while still making one
// task's drain arbitrarily long. Whichever bound binds first, the rest is
// delivered on the following task.
const MaxConsumeItemsPerTask = 1000

// MaxConsumeBytesPerTask bounds one Workflow Task's slice by size. Paired with
// MaxConsumeItemsPerTask because neither bound alone is enough: a burst of tiny
// messages slips under the byte budget, and a few large ones slip under the
// item count.
const MaxConsumeBytesPerTask = 2 << 20

// MaxProducersPerStream bounds the per-producer dedup table. The table is part
// of the component state written on every append, so a caller that sends a
// fresh producer id per request would grow the state until the mutable-state
// size limit rejects every further append, leaving the stream unwritable for
// good. The bound turns that into a clear error on the offending call.
const MaxProducersPerStream = 1000

// MaxConsumersPerStream bounds the registered consumer table for the same
// reason. Each consumer also holds a truncation floor, so an unbounded table
// would pin storage as well as grow state.
const MaxConsumersPerStream = 1000

// MaxListPageSize bounds a visibility page when the caller does not.
const MaxListPageSize = 1000
Loading
Loading