iop/packages/go/streamgate/evidence_tail.go
toki 8a4f6c55a1 sync: roadmap, skills, test inventory, streamgate package, docs updates
- Update roadmap milestones and phase docs across multiple phases
- Update plan, code-review, create-roadmap, update-roadmap, finalize-task-routing skills
- Update dev-corp-runtime-deploy, dev-runtime-deploy, orchestrate-agent-task-loop skills
- Refactor agent-task-loop dispatch script
- Add streamgate Go package (commit_boundary, evidence_tail, filter_registry, stream_release)
- Add test inventory files (dev, dev-corp, unified)
- Update test smoke tests and rules for dev/dev-corp
- Update docs/edge-local-dev-guide and e2e scripts
- Update inventory-query Go package
- Remove deprecated templates and inventory.yaml files
- Add orchestrate-agent-task-loop tests
2026-07-25 11:41:08 +09:00

1902 lines
69 KiB
Go

// Package streamgate provides transport-agnostic event contract types for the
// stream evidence gate. It owns the normalized event lifecycle, immutable
// evidence/filter decision types, and terminal/failure/release payloads.
//
// This package must not import apps/, proto/, or packages/go/config/.
// It depends only on the Go standard library.
package streamgate
import (
"errors"
"fmt"
"sort"
"strings"
"unicode/utf8"
)
// FilterHoldMode identifies how a filter holds pending evidence before
// allowing release. It is a closed set of modes; each mode defines its own
// trigger condition and field requirements.
type FilterHoldMode string
// knownFilterHoldModes is the closed set of valid FilterHoldMode values.
var knownFilterHoldModes = map[FilterHoldMode]struct{}{
FilterHoldModeNone: {},
FilterHoldModeRolling: {},
FilterHoldModeTerminalGate: {},
FilterHoldModeFragmentGate: {},
}
const (
// FilterHoldModeNone indicates no hold. Events pass through immediately.
FilterHoldModeNone FilterHoldMode = "none"
// FilterHoldModeRolling indicates the filter holds until the rolling
// rune evidence window meets a configured threshold. Time is not a
// release condition.
FilterHoldModeRolling FilterHoldMode = "rolling_window"
// FilterHoldModeTerminalGate indicates the filter holds all events until
// a terminal event is received. Terminal or provider-error events serve
// as the trigger.
FilterHoldModeTerminalGate FilterHoldMode = "terminal_gate"
// FilterHoldModeFragmentGate indicates the filter holds tool-call
// fragments until a CompleteFragment signal arrives for the keyed ID.
FilterHoldModeFragmentGate FilterHoldMode = "fragment_gate"
)
// Validate returns nil when the mode is a known lifecycle value.
func (m FilterHoldMode) Validate() error {
switch m {
case FilterHoldModeNone, FilterHoldModeRolling, FilterHoldModeTerminalGate, FilterHoldModeFragmentGate:
return nil
}
return errors.New("streamgate: unknown filter hold mode: " + string(m))
}
// IsValidMode returns true when m is a known hold mode value.
func IsValidMode(m FilterHoldMode) bool {
_, ok := knownFilterHoldModes[m]
return ok
}
// defaultEvidenceRunes is the default Unicode rune evidence window used when
// no policy override is provided.
const defaultEvidenceRunes = 500
// minEvidenceRunes is the absolute minimum allowed rune window.
const minEvidenceRunes = 1
// maxEvidenceRunes is the absolute maximum allowed rune window.
const maxEvidenceRunes = 65536
// defaultMaxBufferRunes is the default hard buffer limit in runes.
const defaultMaxBufferRunes = 4096
// minMaxBufferRunes is the absolute minimum allowed buffer limit.
const minMaxBufferRunes = 10
// maxMaxBufferRunes is the absolute maximum allowed buffer limit.
const maxMaxBufferRunes = 1048576
// FilterHoldRequirement is an immutable specification of how a filter holds
// pending evidence before releasing it. The fields match the SDD naming and
// semantics exactly. A requirement is mode-specific: rolling requires a
// positive rune threshold, terminal gate requires a terminal trigger kind,
// fragment gate requires a fragment trigger kind, and none is a no-op.
type FilterHoldRequirement struct {
channel string
mode FilterHoldMode
subscribedKinds []EventKind
evidenceRunes int
triggerKind EventKind
maxBufferRunes int
}
// NewFilterHoldRequirementNone creates a FilterHoldRequirement of mode none.
// None mode has no rune threshold, no trigger, and releases immediately.
// Subscribed kinds are required but unused.
func NewFilterHoldRequirementNone(channel string, subscribedKinds []EventKind) (FilterHoldRequirement, error) {
if err := validateHoldChannel("none", channel); err != nil {
return FilterHoldRequirement{}, err
}
kinds, err := validateSubscribedKinds("none", subscribedKinds)
if err != nil {
return FilterHoldRequirement{}, err
}
req := FilterHoldRequirement{
channel: channel,
mode: FilterHoldModeNone,
subscribedKinds: kinds,
evidenceRunes: 0,
triggerKind: "",
maxBufferRunes: 0,
}
if err := req.Validate(); err != nil {
return FilterHoldRequirement{}, err
}
return req, nil
}
// NewFilterHoldRequirementRollingWithMaxBuffer creates a FilterHoldRequirement
// of mode rolling_window with an explicit max_buffer_runes override. The
// rune threshold must be within the allowed range. The max_buffer_runes must
// be >= evidence_runes to ensure the buffer can hold the evidence window.
func NewFilterHoldRequirementRollingWithMaxBuffer(channel string, subscribedKinds []EventKind, evidenceRunes int, maxBufferRunes int) (FilterHoldRequirement, error) {
if err := validateHoldChannel("rolling_window", channel); err != nil {
return FilterHoldRequirement{}, err
}
if evidenceRunes < minEvidenceRunes || evidenceRunes > maxEvidenceRunes {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: rolling_window evidence_runes must be between %d and %d, got %d", minEvidenceRunes, maxEvidenceRunes, evidenceRunes)
}
if maxBufferRunes < evidenceRunes {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: rolling_window max_buffer_runes must be >= evidence_runes, got evidence=%d buffer=%d", evidenceRunes, maxBufferRunes)
}
if maxBufferRunes < minMaxBufferRunes || maxBufferRunes > maxMaxBufferRunes {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: rolling_window max_buffer_runes must be between %d and %d, got %d", minMaxBufferRunes, maxMaxBufferRunes, maxBufferRunes)
}
kinds, err := validateSubscribedKinds("rolling_window", subscribedKinds)
if err != nil {
return FilterHoldRequirement{}, err
}
req := FilterHoldRequirement{
channel: channel,
mode: FilterHoldModeRolling,
subscribedKinds: kinds,
evidenceRunes: evidenceRunes,
triggerKind: "",
maxBufferRunes: maxBufferRunes,
}
if err := req.Validate(); err != nil {
return FilterHoldRequirement{}, err
}
return req, nil
}
// NewFilterHoldRequirementRolling creates a FilterHoldRequirement of mode
// rolling_window. The rune threshold must be within the allowed range. The
// subscribed kinds define which event types contribute to the rolling count.
func NewFilterHoldRequirementRolling(channel string, subscribedKinds []EventKind, evidenceRunes int) (FilterHoldRequirement, error) {
return NewFilterHoldRequirementRollingWithMaxBuffer(channel, subscribedKinds, evidenceRunes, defaultMaxBufferRunes)
}
// NewFilterHoldRequirementTerminalGateWithMaxBuffer creates a FilterHoldRequirement
// of mode terminal_gate with an explicit max_buffer_runes override.
func NewFilterHoldRequirementTerminalGateWithMaxBuffer(channel string, subscribedKinds []EventKind, triggerKind EventKind, maxBufferRunes int) (FilterHoldRequirement, error) {
if err := validateHoldChannel("terminal_gate", channel); err != nil {
return FilterHoldRequirement{}, err
}
if triggerKind != EventKindTerminal && triggerKind != EventKindProviderError {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: terminal_gate trigger must be terminal or provider_error, got %q", triggerKind)
}
if maxBufferRunes < minMaxBufferRunes || maxBufferRunes > maxMaxBufferRunes {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: terminal_gate max_buffer_runes must be between %d and %d, got %d", minMaxBufferRunes, maxMaxBufferRunes, maxBufferRunes)
}
kinds, err := validateSubscribedKinds("terminal_gate", subscribedKinds)
if err != nil {
return FilterHoldRequirement{}, err
}
req := FilterHoldRequirement{
channel: channel,
mode: FilterHoldModeTerminalGate,
subscribedKinds: kinds,
evidenceRunes: 0,
triggerKind: triggerKind,
maxBufferRunes: maxBufferRunes,
}
if err := req.Validate(); err != nil {
return FilterHoldRequirement{}, err
}
return req, nil
}
// NewFilterHoldRequirementTerminalGate creates a FilterHoldRequirement of
// mode terminal_gate. The trigger kind must be terminal or provider_error.
func NewFilterHoldRequirementTerminalGate(channel string, subscribedKinds []EventKind, triggerKind EventKind) (FilterHoldRequirement, error) {
return NewFilterHoldRequirementTerminalGateWithMaxBuffer(channel, subscribedKinds, triggerKind, defaultMaxBufferRunes)
}
// NewFilterHoldRequirementFragmentGateWithMaxBuffer creates a FilterHoldRequirement
// of mode fragment_gate with an explicit max_buffer_runes override.
func NewFilterHoldRequirementFragmentGateWithMaxBuffer(channel string, subscribedKinds []EventKind, triggerKind EventKind, maxBufferRunes int) (FilterHoldRequirement, error) {
if err := validateHoldChannel("fragment_gate", channel); err != nil {
return FilterHoldRequirement{}, err
}
if triggerKind != EventKindToolCallFragment {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: fragment_gate trigger must be tool_call_fragment, got %q", triggerKind)
}
if maxBufferRunes < minMaxBufferRunes || maxBufferRunes > maxMaxBufferRunes {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: fragment_gate max_buffer_runes must be between %d and %d, got %d", minMaxBufferRunes, maxMaxBufferRunes, maxBufferRunes)
}
kinds, err := validateSubscribedKinds("fragment_gate", subscribedKinds)
if err != nil {
return FilterHoldRequirement{}, err
}
req := FilterHoldRequirement{
channel: channel,
mode: FilterHoldModeFragmentGate,
subscribedKinds: kinds,
evidenceRunes: 0,
triggerKind: triggerKind,
maxBufferRunes: maxBufferRunes,
}
if err := req.Validate(); err != nil {
return FilterHoldRequirement{}, err
}
return req, nil
}
// NewFilterHoldRequirementFragmentGate creates a FilterHoldRequirement of
// mode fragment_gate. The trigger kind must be tool_call_fragment.
func NewFilterHoldRequirementFragmentGate(channel string, subscribedKinds []EventKind, triggerKind EventKind) (FilterHoldRequirement, error) {
return NewFilterHoldRequirementFragmentGateWithMaxBuffer(channel, subscribedKinds, triggerKind, defaultMaxBufferRunes)
}
func validateHoldChannel(mode, channel string) error {
if channel == "" {
return errors.New("streamgate: hold requirement channel is required")
}
return nil
}
func validateSubscribedKinds(mode string, kinds []EventKind) ([]EventKind, error) {
if len(kinds) == 0 {
return nil, fmt.Errorf("streamgate: %s hold requirement subscribed kinds must not be empty", mode)
}
seen := make(map[EventKind]struct{}, len(kinds))
for _, k := range kinds {
if err := k.Validate(); err != nil {
return nil, fmt.Errorf("streamgate: %s hold requirement subscribed kind: %v", mode, err)
}
if _, dup := seen[k]; dup {
return nil, fmt.Errorf("streamgate: %s hold requirement duplicate subscribed kind: %s", mode, k)
}
seen[k] = struct{}{}
}
out := make([]EventKind, len(kinds))
copy(out, kinds)
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out, nil
}
// Validate returns nil when the requirement is in a consistent state.
// Mode-specific field rules are enforced.
func (r FilterHoldRequirement) Validate() error {
if r.channel == "" {
return errors.New("streamgate: hold requirement channel is required")
}
if err := r.mode.Validate(); err != nil {
return err
}
if len(r.subscribedKinds) == 0 {
return errors.New("streamgate: hold requirement subscribed kinds must not be empty")
}
switch r.mode {
case FilterHoldModeNone:
if r.evidenceRunes != 0 {
return errors.New("streamgate: none mode must not have evidence_runes")
}
if r.triggerKind != "" {
return errors.New("streamgate: none mode must not have a trigger")
}
if r.maxBufferRunes != 0 {
return errors.New("streamgate: none mode must not have max_buffer_runes")
}
case FilterHoldModeRolling:
if r.evidenceRunes < minEvidenceRunes || r.evidenceRunes > maxEvidenceRunes {
return errors.New("streamgate: rolling_window evidence_runes out of range")
}
if r.triggerKind != "" {
return errors.New("streamgate: rolling_window mode must not have a trigger")
}
if r.maxBufferRunes < minMaxBufferRunes || r.maxBufferRunes > maxMaxBufferRunes {
return errors.New("streamgate: rolling_window max_buffer_runes out of range")
}
case FilterHoldModeTerminalGate:
if r.evidenceRunes != 0 {
return errors.New("streamgate: terminal_gate mode must not have evidence_runes")
}
if r.triggerKind != EventKindTerminal && r.triggerKind != EventKindProviderError {
return errors.New("streamgate: terminal_gate trigger must be terminal or provider_error")
}
if r.maxBufferRunes < minMaxBufferRunes || r.maxBufferRunes > maxMaxBufferRunes {
return errors.New("streamgate: terminal_gate max_buffer_runes out of range")
}
case FilterHoldModeFragmentGate:
if r.evidenceRunes != 0 {
return errors.New("streamgate: fragment_gate mode must not have evidence_runes")
}
if r.triggerKind != EventKindToolCallFragment {
return errors.New("streamgate: fragment_gate trigger must be tool_call_fragment")
}
if r.maxBufferRunes < minMaxBufferRunes || r.maxBufferRunes > maxMaxBufferRunes {
return errors.New("streamgate: fragment_gate max_buffer_runes out of range")
}
}
return nil
}
// Channel returns the requirement channel key.
func (r FilterHoldRequirement) Channel() string { return r.channel }
// Mode returns the hold mode.
func (r FilterHoldRequirement) Mode() FilterHoldMode { return r.mode }
// SubscribedKinds returns a defensive copy of the subscribed event kinds.
func (r FilterHoldRequirement) SubscribedKinds() []EventKind {
if r.subscribedKinds == nil {
return nil
}
out := make([]EventKind, len(r.subscribedKinds))
copy(out, r.subscribedKinds)
return out
}
// EvidenceRunes returns the rolling rune threshold. Returns 0 for non-rolling
// modes.
func (r FilterHoldRequirement) EvidenceRunes() int { return r.evidenceRunes }
// TriggerKind returns the trigger event kind. Returns "" for modes that do
// not use a trigger.
func (r FilterHoldRequirement) TriggerKind() EventKind { return r.triggerKind }
// MaxBufferRunes returns the hard buffer limit in runes.
func (r FilterHoldRequirement) MaxBufferRunes() int { return r.maxBufferRunes }
// IsBlocking returns true when this requirement's mode blocks channel release.
// None and observe-only modes are non-blocking; rolling, terminal_gate, and
// fragment_gate are blocking.
func (r FilterHoldRequirement) IsBlocking() bool {
switch r.mode {
case FilterHoldModeRolling, FilterHoldModeTerminalGate, FilterHoldModeFragmentGate:
return true
default:
return false
}
}
// FilterHoldBinding pairs a stable filter ID with its requirement and an
// explicit BlocksRelease flag resolved by the caller. The binding is
// immutable to callers.
type FilterHoldBinding struct {
filterID StableToken
requirement FilterHoldRequirement
blocksRelease bool
}
// NewFilterHoldBinding creates a FilterHoldBinding with validation. The
// returned value is immutable to callers.
func NewFilterHoldBinding(filterID string, req FilterHoldRequirement, blocksRelease bool) (FilterHoldBinding, error) {
f, err := NewStableTokenRequired("filterID", filterID)
if err != nil {
return FilterHoldBinding{}, err
}
if err := req.Validate(); err != nil {
return FilterHoldBinding{}, err
}
return FilterHoldBinding{
filterID: f,
requirement: req,
blocksRelease: blocksRelease,
}, nil
}
// FilterID returns the stable filter id token.
func (b FilterHoldBinding) FilterID() string { return b.filterID.value }
// Requirement returns the hold requirement.
func (b FilterHoldBinding) Requirement() FilterHoldRequirement { return b.requirement }
// BlocksRelease returns whether this binding blocks channel release.
func (b FilterHoldBinding) BlocksRelease() bool { return b.blocksRelease }
// evidencePlanKey is the internal key for grouping bindings by channel.
type evidencePlanKey struct {
channel string
mode FilterHoldMode
}
// evidencePlan holds the compiled per-channel hold plan derived from bindings.
// It only tracks blocking requirements; observe-only bindings contribute to
// observation only and never affect the channel hold decision.
type evidencePlan struct {
// blockingRequirements is keyed by channel, value is the strongest
// blocking requirement found for that channel. "Strongest" follows a
// deterministic priority: terminal_gate > fragment_gate > rolling_window.
blockingRequirements map[string]FilterHoldRequirement
// observeOnlyKinds is the union of subscribed event kinds from all
// non-blocking (observe-only) bindings per channel.
observeOnlyKinds map[string][]EventKind
// allKinds is the union of all subscribed kinds (blocking + observe) per
// channel. Used for deciding which events to route.
allKinds map[string][]EventKind
// blockingKinds is the union of subscribed kinds from all blocking
// bindings per channel. Only these kinds contribute to pending state.
blockingKinds map[string][]EventKind
}
// compileEvidencePlan produces a compiled plan from a slice of bindings.
// Only blocking bindings affect the hold decision; observe-only bindings
// do not create a channel hold. Weaker blocking bindings contribute kinds
// that are not in the strongest binding's subscribedKinds to observeOnlyKinds.
func compileEvidencePlan(bindings []FilterHoldBinding) (evidencePlan, error) {
plan := evidencePlan{
blockingRequirements: make(map[string]FilterHoldRequirement),
observeOnlyKinds: make(map[string][]EventKind),
allKinds: make(map[string][]EventKind),
blockingKinds: make(map[string][]EventKind),
}
// Track the strongest binding's original subscribedKinds per channel so
// we can determine which weaker binding kinds become observe-only.
type strongestInfo struct {
originalSubscribed []EventKind
}
strongestMap := make(map[string]strongestInfo)
for _, b := range bindings {
req := b.requirement
ch := req.Channel()
// Merge kinds for this channel.
plan.allKinds[ch] = mergeKinds(plan.allKinds[ch], req.SubscribedKinds())
if !b.blocksRelease || !req.IsBlocking() {
plan.observeOnlyKinds[ch] = mergeKinds(plan.observeOnlyKinds[ch], req.SubscribedKinds())
continue
}
// For blocking bindings, merge the blocking kinds union too.
plan.blockingKinds[ch] = mergeKinds(plan.blockingKinds[ch], req.SubscribedKinds())
// For blocking bindings, merge deterministically per channel.
existing, exists := plan.blockingRequirements[ch]
if !exists {
plan.blockingRequirements[ch] = req
strongestMap[ch] = strongestInfo{originalSubscribed: req.SubscribedKinds()}
continue
}
if strongerMode(req.Mode(), existing.Mode()) {
// New strongest: kinds from old strongest that aren't in new strongest's
// subscribedKinds become observe-only.
oldKinds := strongestMap[ch].originalSubscribed
for _, k := range oldKinds {
if !isKindSubscribed(k, req.SubscribedKinds()) {
plan.observeOnlyKinds[ch] = appendUniqueKind(plan.observeOnlyKinds[ch], k)
}
}
merged, err := mergeBlockingRequirement(existing, req)
if err != nil {
return evidencePlan{}, err
}
plan.blockingRequirements[ch] = merged
strongestMap[ch] = strongestInfo{originalSubscribed: req.SubscribedKinds()}
} else {
// Existing strongest: kinds from this (weaker) binding that aren't in
// strongest's subscribedKinds become observe-only.
strongestKinds := strongestMap[ch].originalSubscribed
for _, k := range req.SubscribedKinds() {
if !isKindSubscribed(k, strongestKinds) {
plan.observeOnlyKinds[ch] = appendUniqueKind(plan.observeOnlyKinds[ch], k)
}
}
merged, err := mergeBlockingRequirement(existing, req)
if err != nil {
return evidencePlan{}, err
}
plan.blockingRequirements[ch] = merged
}
}
return plan, nil
}
// isKindSubscribed returns true when kind is in the subscribed kinds slice.
func isKindSubscribed(kind EventKind, subscribed []EventKind) bool {
for _, k := range subscribed {
if k == kind {
return true
}
}
return false
}
// appendUniqueKind appends kind to existing only if not already present.
// This is intentionally lightweight (no sort) since we only de-dup, not
// order. Ordering is handled by mergeKinds where sorted output is required.
func appendUniqueKind(existing []EventKind, kind EventKind) []EventKind {
for _, k := range existing {
if k == kind {
return existing
}
}
return append(existing, kind)
}
// strongerMode returns true when a is strictly stronger than b in the
// hold priority: terminal_gate > fragment_gate > rolling_window.
func strongerMode(a, b FilterHoldMode) bool {
priority := map[FilterHoldMode]int{
FilterHoldModeRolling: 1,
FilterHoldModeFragmentGate: 2,
FilterHoldModeTerminalGate: 3,
}
return priority[a] > priority[b]
}
// mergeBlockingRequirement merges two blocking requirements for the same
// channel into a single deterministic requirement. The merged result preserves
// the subscribed kinds union, uses max for evidence threshold and min for hard
// bounds, and rejects incompatible combinations. When modes differ, the
// merged result takes the strongest mode but unions kinds and mins positive
// bounds across both requirements. Same-mode terminal triggers must match.
func mergeBlockingRequirement(existing, next FilterHoldRequirement) (FilterHoldRequirement, error) {
if existing.Channel() != next.Channel() {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: cannot merge requirements for different channels: %s vs %s", existing.Channel(), next.Channel())
}
// When modes differ, the strongest mode wins. All subscription kinds are
// unioned and all positive hard bounds are minimized.
if existing.Mode() != next.Mode() {
merged := FilterHoldRequirement{
channel: existing.Channel(),
subscribedKinds: mergeKinds(existing.SubscribedKinds(), next.SubscribedKinds()),
}
if strongerMode(next.Mode(), existing.Mode()) {
merged.mode = next.Mode()
merged.triggerKind = next.triggerKind
} else {
merged.mode = existing.Mode()
merged.triggerKind = existing.triggerKind
}
merged.maxBufferRunes = minPositiveBound(existing.MaxBufferRunes(), next.MaxBufferRunes())
if merged.mode == FilterHoldModeRolling {
merged.evidenceRunes = maxInt(existing.EvidenceRunes(), next.EvidenceRunes())
}
if err := validateCompatibleTrigger(existing, next); err != nil {
return FilterHoldRequirement{}, err
}
if err := merged.Validate(); err != nil {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: merged requirement invalid: %v", err)
}
return merged, nil
}
// Same mode: merge deterministically.
merged := FilterHoldRequirement{
channel: existing.Channel(),
mode: existing.Mode(),
subscribedKinds: mergeKinds(existing.SubscribedKinds(), next.SubscribedKinds()),
triggerKind: existing.TriggerKind(),
}
switch existing.Mode() {
case FilterHoldModeRolling:
// Use max for evidence threshold, min for hard buffer.
if existing.EvidenceRunes() > next.EvidenceRunes() {
merged.evidenceRunes = existing.EvidenceRunes()
} else {
merged.evidenceRunes = next.EvidenceRunes()
}
if existing.MaxBufferRunes() < next.MaxBufferRunes() {
merged.maxBufferRunes = existing.MaxBufferRunes()
} else {
merged.maxBufferRunes = next.MaxBufferRunes()
}
if merged.maxBufferRunes < merged.evidenceRunes {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: merged rolling buffer %d < evidence threshold %d", merged.maxBufferRunes, merged.evidenceRunes)
}
case FilterHoldModeTerminalGate:
// Same terminal mode: triggers must match.
if err := validateCompatibleTrigger(existing, next); err != nil {
return FilterHoldRequirement{}, err
}
if existing.MaxBufferRunes() < next.MaxBufferRunes() {
merged.maxBufferRunes = existing.MaxBufferRunes()
} else {
merged.maxBufferRunes = next.MaxBufferRunes()
}
case FilterHoldModeFragmentGate:
// Same fragment mode: triggers must match.
if err := validateCompatibleTrigger(existing, next); err != nil {
return FilterHoldRequirement{}, err
}
if existing.MaxBufferRunes() < next.MaxBufferRunes() {
merged.maxBufferRunes = existing.MaxBufferRunes()
} else {
merged.maxBufferRunes = next.MaxBufferRunes()
}
}
if err := merged.Validate(); err != nil {
return FilterHoldRequirement{}, fmt.Errorf("streamgate: merged requirement invalid: %v", err)
}
return merged, nil
}
// validateCompatibleTrigger rejects two requirements whose terminal triggers
// are incompatible: different non-none terminal triggers for the same mode.
func validateCompatibleTrigger(a, b FilterHoldRequirement) error {
aTrigger := a.TriggerKind()
bTrigger := b.TriggerKind()
if aTrigger == "" && bTrigger == "" {
return nil
}
if aTrigger == bTrigger {
return nil
}
// One or both have a trigger; if they differ, reject.
if aTrigger != "" && bTrigger != "" && aTrigger != bTrigger {
return fmt.Errorf("streamgate: incompatible terminal triggers %q vs %q", aTrigger, bTrigger)
}
return nil
}
// minPositiveBound returns the smaller of two positive values; if either is
// zero (e.g. none mode has no hard bound) it returns the other.
func minPositiveBound(a, b int) int {
if a == 0 {
return b
}
if b == 0 {
return a
}
if a < b {
return a
}
return b
}
// maxInt returns the larger of two ints.
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func mergeKinds(a, b []EventKind) []EventKind {
seen := make(map[EventKind]struct{}, len(a)+len(b))
var out []EventKind
for _, k := range a {
if _, ok := seen[k]; !ok {
seen[k] = struct{}{}
out = append(out, k)
}
}
for _, k := range b {
if _, ok := seen[k]; !ok {
seen[k] = struct{}{}
out = append(out, k)
}
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}
// EvidencePlan is the compiled channel plan for evidence holding. It is
// immutable to callers once created. The plan only includes blocking
// requirements; observe-only and none-mode bindings never create a channel
// hold on their own.
type EvidencePlan struct {
plan evidencePlan
}
// NewEvidencePlan compiles a channel hold plan from the provided bindings.
// Only blocking bindings affect the hold decision. Returns an error if any
// binding's requirement fails validation.
func NewEvidencePlan(bindings []FilterHoldBinding) (EvidencePlan, error) {
if len(bindings) == 0 {
return EvidencePlan{plan: evidencePlan{
blockingRequirements: make(map[string]FilterHoldRequirement),
observeOnlyKinds: make(map[string][]EventKind),
allKinds: make(map[string][]EventKind),
blockingKinds: make(map[string][]EventKind),
}}, nil
}
compiled, err := compileEvidencePlan(bindings)
if err != nil {
return EvidencePlan{}, err
}
return EvidencePlan{plan: compiled}, nil
}
// BlockingRequirementFor returns the strongest blocking requirement for the
// channel and true, or an empty requirement and false if no blocking
// requirement exists.
func (p EvidencePlan) BlockingRequirementFor(channel string) (FilterHoldRequirement, bool) {
req, ok := p.plan.blockingRequirements[channel]
return req, ok
}
// HasBlockingRequirement returns true when the given channel has at least one
// blocking requirement in the plan.
func (p EvidencePlan) HasBlockingRequirement(channel string) bool {
_, ok := p.plan.blockingRequirements[channel]
return ok
}
// BlockingRequirement returns the strongest blocking requirement for the
// channel, or an error if no blocking requirement exists.
func (p EvidencePlan) BlockingRequirement(channel string) (FilterHoldRequirement, error) {
req, ok := p.plan.blockingRequirements[channel]
if !ok {
return FilterHoldRequirement{}, errors.New("streamgate: no blocking requirement for channel: " + channel)
}
return req, nil
}
// SubscribeKinds returns a defensive copy of the merged subscribed kinds for the channel.
func (p EvidencePlan) SubscribeKinds(channel string) []EventKind {
kinds := p.plan.allKinds[channel]
if kinds == nil {
return nil
}
out := make([]EventKind, len(kinds))
copy(out, kinds)
return out
}
// BlockingSubscribeKinds returns a defensive copy of the blocking subscribed
// kinds for the channel. These are the kinds that contribute to pending state.
// Returns nil when the channel has no blocking requirement.
func (p EvidencePlan) BlockingSubscribeKinds(channel string) []EventKind {
kinds := p.plan.blockingKinds[channel]
if kinds == nil {
return nil
}
out := make([]EventKind, len(kinds))
copy(out, kinds)
return out
}
// BlockingKinds returns the union of event kinds that contribute to the
// channel's blocking pending state. Returns nil when the channel has no
// blocking requirement.
func (p EvidencePlan) BlockingKinds(channel string) []EventKind {
kinds := p.plan.blockingKinds[channel]
if kinds == nil {
return nil
}
out := make([]EventKind, len(kinds))
copy(out, kinds)
return out
}
// ObserveKinds returns the union of event kinds that contribute to
// observation-only subscriptions but never to blocking pending state.
// Returns nil when the channel has no observe-only subscription.
func (p EvidencePlan) ObserveKinds(channel string) []EventKind {
kinds := p.plan.observeOnlyKinds[channel]
if kinds == nil {
return nil
}
out := make([]EventKind, len(kinds))
copy(out, kinds)
return out
}
// EvidenceEpoch is an immutable snapshot of evidence state at the moment of
// a state transition (e.g. threshold reached, trigger fired). It carries
// only safe, externally-facing identification.
type EvidenceEpoch struct {
id uint64
channel string
mode FilterHoldMode
triggered bool
reason string
}
// NewEvidenceEpoch creates an EvidenceEpoch with the given parameters.
func NewEvidenceEpoch(id uint64, channel string, mode FilterHoldMode, triggered bool, reason string) (EvidenceEpoch, error) {
if channel == "" {
return EvidenceEpoch{}, errors.New("streamgate: epoch channel is required")
}
if err := mode.Validate(); err != nil {
return EvidenceEpoch{}, err
}
return EvidenceEpoch{
id: id,
channel: channel,
mode: mode,
triggered: triggered,
reason: reason,
}, nil
}
// ID returns the epoch identifier.
func (e EvidenceEpoch) ID() uint64 { return e.id }
// Channel returns the epoch channel.
func (e EvidenceEpoch) Channel() string { return e.channel }
// Mode returns the epoch hold mode.
func (e EvidenceEpoch) Mode() FilterHoldMode { return e.mode }
// Triggered returns whether the epoch was a trigger event.
func (e EvidenceEpoch) Triggered() bool { return e.triggered }
// Reason returns the epoch reason string.
func (e EvidenceEpoch) Reason() string { return e.reason }
// EvidenceTailSignal identifies the kind of signal emitted by evidence tail
// operations.
type EvidenceTailSignal string
const (
// EvidenceTailSignalNone indicates no signal was produced.
EvidenceTailSignalNone EvidenceTailSignal = "none"
// EvidenceTailSignalThreshold indicates the rolling rune threshold was
// reached and a batch is ready.
EvidenceTailSignalThreshold EvidenceTailSignal = "threshold"
// EvidenceTailSignalTrigger indicates the trigger event (terminal or
// fragment) was received.
EvidenceTailSignalTrigger EvidenceTailSignal = "trigger"
// EvidenceTailSignalBufferOverflow indicates the hard buffer limit was
// exceeded. No release is produced.
EvidenceTailSignalBufferOverflow EvidenceTailSignal = "overflow"
// EvidenceTailSignalReady indicates the state is ready for release
// (used when all accumulated events are eligible).
EvidenceTailSignalReady EvidenceTailSignal = "ready"
)
// EvidenceTailSignalOverflow is a typed signal for buffer overflow. It carries
// only the channel, bound, and cause code; no raw content.
type EvidenceTailSignalOverflow struct {
channel string
bound int
code string
}
// NewEvidenceTailSignalOverflow creates a typed overflow signal with the
// given parameters.
func NewEvidenceTailSignalOverflow(channel string, bound int, code string) (EvidenceTailSignalOverflow, error) {
if channel == "" {
return EvidenceTailSignalOverflow{}, errors.New("streamgate: overflow signal channel is required")
}
if bound <= 0 {
return EvidenceTailSignalOverflow{}, errors.New("streamgate: overflow signal bound must be positive")
}
if code == "" {
return EvidenceTailSignalOverflow{}, errors.New("streamgate: overflow signal cause code is required")
}
return EvidenceTailSignalOverflow{
channel: channel,
bound: bound,
code: code,
}, nil
}
// Channel returns the overflow channel.
func (s EvidenceTailSignalOverflow) Channel() string { return s.channel }
// Bound returns the overflow rune bound.
func (s EvidenceTailSignalOverflow) Bound() int { return s.bound }
// Code returns the overflow cause code.
func (s EvidenceTailSignalOverflow) Code() string { return s.code }
// EvidenceTail manages per-channel pending/look-behind state and tool-call
// fragment state for evidence holding. It is the core state machine that
// accumulates normalized events, validates UTF-8, counts Unicode runes, and
// produces epochs when thresholds or triggers are met.
type EvidenceTail struct {
plan EvidencePlan
epochCounter uint64
// channelState is keyed by channel name.
channelState map[string]*channelState
// epochs tracks epoch records by ID for validation during PrepareRelease
// and ConfirmRelease. Each epoch is bound to a specific channel and can
// only be prepared once.
epochs map[uint64]*epochRecord
// preparedByChannel tracks which channel has a pending prepared release
// for each epoch, preventing overlapping prepared tokens.
preparedByChannel map[string]string
// tokenNonce is a monotonic counter that ensures every generated release
// token is globally unique. It prevents stale token reuse when the same
// epoch is re-prepared after zero or partial confirm.
tokenNonce uint64
}
// channelState holds the runtime state for a single channel.
type channelState struct {
// pendingEntries is the ordered list of pending entry records for this channel.
// Each entry tracks the event, its rune count, and associated fragment ID (if any).
pendingEntries []pendingEntry
// nextSequence is a monotonic counter that assigns an immutable identity
// to each pending entry. It is never decremented or reused, even after
// confirm removes entries from the front. This allows epoch records to
// anchor their snapshot to a fixed sequence range rather than a mutable
// slice index.
nextSequence int
// committedLookBehind is the bounded list of events already confirmed
// released downstream. Used for cross-boundary repeat detection.
committedLookBehind []NormalizedEvent
// committedCursor is the total number of events confirmed released
// downstream. This is a monotonic counter that increases with each
// successful ConfirmRelease and resets on replace.
committedCursor int
// pendingRunes is the cumulative Unicode rune count of pendingEntries.
pendingRunes int
// effectiveEvidenceRunes is the rolling evidence window used for
// look-behind trimming on this channel. For non-rolling modes it equals
// maxBufferRunes.
effectiveEvidenceRunes int
// fragmentState tracks incomplete tool-call fragments keyed by toolCallID.
fragmentState map[string]*fragmentState
// maxBufferRunes is the hard buffer limit for this channel.
maxBufferRunes int
}
// pendingEntry represents a single pending event with its metadata.
type pendingEntry struct {
event NormalizedEvent
runes int
kind EventKind
toolCallID string // empty if not a fragment
sequence int // monotonic sequence number
}
// fragmentState tracks an incomplete tool-call fragment.
// entries is the ordered list of pending entry sequences for this fragment ID.
// runes is the total rune count of all entries.
type fragmentState struct {
toolCallID string
entries []int // sequence numbers of pending entries
runes int
completed bool // true when CompleteFragment has been called for this ID
}
// epochRecord tracks an epoch's state through its lifecycle. Each epoch
// is bound to an exact pending-entry sequence range captured at creation
// time; remainingSnapshot returns only entries within that range that
// have not yet been confirmed.
//
// A record is valid for PrepareRelease only when:
// - consumed is false
// - invalidated is false
// - prepared and token carry a pending token whose channel matches
//
// Once invalidated (by recovery/continuation/terminal discard or a newer
// completion epoch), all token/prepared state is cleared and ConfirmRelease
// will reject any stale outstanding token with a deterministic error.
type epochRecord struct {
epochID uint64
channel string
mode FilterHoldMode // the hold mode at epoch creation
consumed bool // true after full confirm or replace
prepared bool // true after successful PrepareRelease
token string // the prepared release token, if any
snapshotSize int // number of events captured in the prepared snapshot
snapStartSeq int // first pending-entry monotonic sequence in the snapshot
snapEndSeq int // last pending-entry monotonic sequence (exclusive) in the snapshot
confirmedCount int // total events confirmed for this epoch across partial confirms
invalidated bool // true after ResetForReplace/PrepareContinuation/DiscardPendingForTerminal
}
// NewEvidenceTail creates a new EvidenceTail from the given compiled plan.
// It initializes per-channel state on first use.
func NewEvidenceTail(plan EvidencePlan) (*EvidenceTail, error) {
if err := plan.validate(); err != nil {
return nil, err
}
return &EvidenceTail{
plan: plan,
epochCounter: 0,
channelState: make(map[string]*channelState),
epochs: make(map[uint64]*epochRecord),
preparedByChannel: make(map[string]string),
tokenNonce: 0,
}, nil
}
// validate checks the plan is consistent.
func (p EvidencePlan) validate() error {
for ch, req := range p.plan.blockingRequirements {
if err := req.Validate(); err != nil {
return fmt.Errorf("streamgate: compiled plan channel %s: %v", ch, err)
}
}
return nil
}
var (
// errStalePreparedRelease is returned by ConfirmRelease when the token
// has already been consumed, invalidated, or does not match the current
// prepared state for the epoch's channel.
errStalePreparedRelease = errors.New("streamgate: stale prepared release token")
// errPreparedSnapshotMismatch is returned when the current pending
// prefix does not match the sequence range captured at PrepareRelease.
errPreparedSnapshotMismatch = errors.New("streamgate: prepared snapshot prefix mismatch")
)
// nextObservationEpoch generates an epoch ID for a non-blocking observation
// event. No epoch record is created; the epoch cannot be prepared or
// confirmed. This is used for events that pass through (unsubscribed,
// observe-only, sub-threshold, overflow) and must never acquire release
// capability.
func (t *EvidenceTail) nextObservationEpoch(channel string, mode FilterHoldMode, triggered bool, reason string) (EvidenceEpoch, error) {
t.epochCounter++
epoch, err := NewEvidenceEpoch(t.epochCounter, channel, mode, triggered, reason)
if err != nil {
return EvidenceEpoch{}, err
}
return epoch, nil
}
// nextReadyEpochRecord generates an epoch ID, creates a release-capable
// epoch record bound to the exact monotonic sequence range [firstSeq, lastSeq),
// and stores it for PrepareRelease/ConfirmRelease. Only ready transitions
// (threshold reached, trigger fired, fragment completion) should call this.
func (t *EvidenceTail) nextReadyEpochRecord(channel string, mode FilterHoldMode, triggered bool, reason string, firstSeq, lastSeqExclusive int) (EvidenceEpoch, *epochRecord, error) {
t.epochCounter++
epoch, err := NewEvidenceEpoch(t.epochCounter, channel, mode, triggered, reason)
if err != nil {
return EvidenceEpoch{}, nil, err
}
record := &epochRecord{
epochID: t.epochCounter,
channel: channel,
mode: mode,
consumed: false,
prepared: false,
token: "",
snapshotSize: 0,
snapStartSeq: firstSeq,
snapEndSeq: lastSeqExclusive,
}
t.epochs[t.epochCounter] = record
return epoch, record, nil
}
// unconfirmedSnapshot returns only the release events within the epoch's
// fixed sequence range that are still present in the current pending slice
// (i.e., not yet confirmed). It uses monotonic sequence identity rather
// than mutable slice indices, so it is safe to call after partial confirm.
func (cs *channelState) unconfirmedSnapshot(record *epochRecord) []ReleaseEvent {
var releaseEvents []ReleaseEvent
for _, entry := range cs.pendingEntries {
if entry.sequence >= record.snapStartSeq && entry.sequence < record.snapEndSeq {
re, err := normalizedToReleaseEvent(entry.event)
if err == nil {
releaseEvents = append(releaseEvents, re)
}
}
}
return releaseEvents
}
// validatePreparedOwnership checks whether the given token matches the
// currently prepared release for the epoch's channel. Returns nil when the
// token is valid and still pending; returns errStalePreparedRelease otherwise.
func (t *EvidenceTail) validatePreparedOwnership(record *epochRecord, token string) error {
if record.invalidated {
return errStalePreparedRelease
}
if t.preparedByChannel[record.channel] != token {
return errStalePreparedRelease
}
return nil
}
// pendingPrefixMatchesRange verifies that the n leading entries of the
// channel's current pending slice have monotonic sequences exactly equal
// to [snapStartSeq, snapStartSeq+n). This guards against a prepared token
// being confirmed after the pending prefix has diverged from the original
// snapshot.
func (cs *channelState) pendingPrefixMatchesRange(n int, snapStartSeq int) bool {
if n > len(cs.pendingEntries) {
return false
}
for i := 0; i < n; i++ {
if cs.pendingEntries[i].sequence != snapStartSeq+i {
return false
}
}
return true
}
// getOrCreateChannel returns the channel state, creating it if needed.
func (t *EvidenceTail) getOrCreateChannel(channel string, maxBuffer int, evidenceRunes int) (*channelState, error) {
cs, ok := t.channelState[channel]
if !ok {
// For rolling modes effectiveEvidenceRunes is the evidence window;
// for non-rolling modes (terminal_gate, fragment_gate, none) it falls
// back to maxBufferRunes so look-behind trimming uses the hard bound.
effective := evidenceRunes
if effective <= 0 {
effective = maxBuffer
}
cs = &channelState{
pendingEntries: make([]pendingEntry, 0),
fragmentState: make(map[string]*fragmentState),
maxBufferRunes: maxBuffer,
effectiveEvidenceRunes: effective,
}
t.channelState[channel] = cs
}
return cs, nil
}
// Append adds a normalized event to the evidence tail. It validates UTF-8,
// counts Unicode runes, updates pending/look-behind state, and checks for
// threshold/trigger/overflow conditions. It returns an epoch and signal if a
// state transition occurred.
//
// The Append function first validates the event, then checks subscription
// applicability. Non-subscribed events pass through without mutating state.
// Observe-only (non-blocking) kinds pass through with no state mutation and
// no epoch. Subscribed blocking events are validated for UTF-8, counted by
// Unicode runes, and added to pending state with overflow protection.
//
// For terminal_gate mode, trigger events (terminal/provider-error) are
// detected BEFORE adding to pending so that nextSequence remains strictly
// monotonic and the control event never appears in release snapshots.
func (t *EvidenceTail) Append(event NormalizedEvent) (EvidenceEpoch, EvidenceTailSignal, error) {
if err := event.Validate(); err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, fmt.Errorf("streamgate: append event validate: %v", err)
}
channel := event.Channel()
req, hasBlocking := t.plan.BlockingRequirementFor(channel)
// Fail-closed: validate UTF-8 before returning Ready or pass-through.
// Non-subscribed or invalid events never mutate hold state.
if err := validateEventUTF8(event); err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, err
}
if !hasBlocking {
// No blocking requirement: events pass through immediately.
epoch, _ := t.nextObservationEpoch(channel, FilterHoldModeNone, false, "pass_through_no_requirement")
return epoch, EvidenceTailSignalReady, nil
}
// For terminal_gate mode, configured trigger events (terminal/provider-error)
// are detected BEFORE subscription checks so the control event never
// enters pending state. UTF-8 and plan validation already passed above.
if req.Mode() == FilterHoldModeTerminalGate && event.Kind() == req.TriggerKind() {
cs, err := t.getOrCreateChannel(channel, req.MaxBufferRunes(), req.EvidenceRunes())
if err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, err
}
// Trigger event is a control event: do NOT add to pending.
// Create a ready epoch for the safe prefix of data events only.
safeEnd := cs.nextSequence
firstSeq := cs.firstPendingSequence()
epoch, _, err := t.nextReadyEpochRecord(
channel, FilterHoldModeTerminalGate, true, "configured_trigger",
firstSeq, safeEnd,
)
if err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, err
}
return epoch, EvidenceTailSignalTrigger, nil
}
// Check subscription applicability. Only blocking kinds contribute to
// pending state. Observe-only kinds pass through without mutating state.
blockingKinds := t.plan.BlockingKinds(channel)
observeKinds := t.plan.ObserveKinds(channel)
if !isKindSubscribed(event.Kind(), blockingKinds) {
// Non-subscribed or observe-only event: no state mutation.
epoch, _ := t.nextObservationEpoch(channel, req.Mode(), false, "event_not_subscribed")
return epoch, EvidenceTailSignalNone, nil
}
// Observe-only event (subscribed but not blocking): pass through.
if isKindSubscribed(event.Kind(), observeKinds) {
epoch, _ := t.nextObservationEpoch(channel, req.Mode(), false, "observe_only_pass_through")
return epoch, EvidenceTailSignalNone, nil
}
cs, err := t.getOrCreateChannel(channel, req.MaxBufferRunes(), req.EvidenceRunes())
if err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, err
}
// Count runes contributed by this event.
runesAdded := runeCountForEvent(event)
newPendingRunes := cs.pendingRunes + runesAdded
// Check hard buffer overflow before adding.
if newPendingRunes > cs.maxBufferRunes {
// Overflow: create observation epoch (no release capability) and signal overflow.
// The typed overflow signal is not required for the public return path;
// only the EvidenceTailSignalBufferOverflow constant is emitted.
epoch, _ := t.nextObservationEpoch(channel, req.Mode(), false, "overflow_beyond_buffer_limit")
return epoch, EvidenceTailSignalBufferOverflow, nil
}
// Add event to pending entry with monotonic sequence identity.
seq := cs.nextSequence
cs.nextSequence++
entry := pendingEntry{
event: cloneNormalizedEvent(event),
runes: runesAdded,
kind: event.Kind(),
sequence: seq,
}
// Extract toolCallID for fragment events.
if event.Kind() == EventKindToolCallFragment {
tc, err := event.AsToolCallFragment()
if err == nil {
entry.toolCallID = tc.ID
// Accumulate fragment state.
fragment := cs.fragmentState[tc.ID]
if fragment == nil {
fragment = &fragmentState{
toolCallID: tc.ID,
entries: []int{},
runes: 0,
completed: false,
}
cs.fragmentState[tc.ID] = fragment
}
newFragmentRunes := fragment.runes + runesAdded
if newFragmentRunes > cs.maxBufferRunes {
// Per-ID overflow: no pending state, no epoch, no release.
return EvidenceEpoch{}, EvidenceTailSignalBufferOverflow, nil
}
fragment.entries = append(fragment.entries, entry.sequence)
fragment.runes = newFragmentRunes
}
}
cs.pendingEntries = append(cs.pendingEntries, entry)
cs.pendingRunes = newPendingRunes
epoch, signal, err := t.completeFragmentGateAndRolling(
channel, req, cs, event, runesAdded,
)
if err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, err
}
return epoch, signal, nil
}
// firstPendingSequence returns the monotonic sequence of the first pending
// entry, or 0 if the pending slice is empty. This is used to anchor epoch
// records to the actual sequence range of the current pending prefix.
func (cs *channelState) firstPendingSequence() int {
if len(cs.pendingEntries) == 0 {
return 0
}
return cs.pendingEntries[0].sequence
}
// completeFragmentGateAndRolling checks trigger/threshold conditions for
// terminal_gate and rolling modes. For terminal_gate, it detects trigger
// events before they enter pending so that the control event is excluded
// from the release snapshot and nextSequence remains strictly monotonic.
// For rolling, it checks if the rune threshold is reached.
func (t *EvidenceTail) completeFragmentGateAndRolling(
channel string, req FilterHoldRequirement, cs *channelState, event NormalizedEvent, runesAdded int,
) (EvidenceEpoch, EvidenceTailSignal, error) {
var signal EvidenceTailSignal = EvidenceTailSignalNone
var epoch EvidenceEpoch
switch req.Mode() {
case FilterHoldModeRolling:
if cs.pendingRunes >= req.EvidenceRunes() {
// Rolling threshold reached: create a ready epoch for the
// current safe prefix (all data events accumulated so far).
safeEnd := cs.nextSequence // exclusive bound for all current entries
firstSeq := cs.firstPendingSequence()
var err error
epoch, _, err = t.nextReadyEpochRecord(
channel, FilterHoldModeRolling, true, "rolling_threshold_reached",
firstSeq, safeEnd,
)
if err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, err
}
signal = EvidenceTailSignalThreshold
}
case FilterHoldModeTerminalGate:
if event.Kind() == req.TriggerKind() {
// Trigger event is a control event: do NOT add to pending.
// Revert the append above to keep the trigger out of the release snapshot.
cs.pendingEntries = cs.pendingEntries[:len(cs.pendingEntries)-1]
cs.pendingRunes = cs.pendingRunes - runesAdded
// Create a ready epoch for the safe prefix of data events only.
safeEnd := cs.nextSequence
firstSeq := cs.firstPendingSequence()
var err error
epoch, _, err = t.nextReadyEpochRecord(
channel, FilterHoldModeTerminalGate, true, "configured_trigger",
firstSeq, safeEnd,
)
if err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, err
}
signal = EvidenceTailSignalTrigger
}
case FilterHoldModeFragmentGate:
// Fragment gate is triggered externally via CompleteFragment.
// Just accumulate.
}
return epoch, signal, nil
}
// CompleteFragment signals that a tool-call fragment is complete for the
// given channel and toolCallID. It returns the completed status, the
// EvidenceEpoch if a contiguous safe prefix was created, a signal indicating
// the kind of transition, and any error. A new epoch is created when the
// completion makes the prefix up to the first incomplete fragment (or all
// entries) contiguous with completed fragments.
//
// When a new safe-prefix epoch is created, any previously prepared (but
// unconfirmed) token on the same channel is invalidated so that stale
// snapshots cannot be re-prepared after a more-complete epoch supersedes them.
func (t *EvidenceTail) CompleteFragment(
channel, toolCallID string,
) (EvidenceEpoch, EvidenceTailSignal, bool, error) {
if channel == "" {
return EvidenceEpoch{}, EvidenceTailSignalNone, false, errors.New("streamgate: complete fragment channel is required")
}
if toolCallID == "" {
return EvidenceEpoch{}, EvidenceTailSignalNone, false, errors.New("streamgate: complete fragment tool call id is required")
}
req, hasBlocking := t.plan.BlockingRequirementFor(channel)
if !hasBlocking {
return EvidenceEpoch{}, EvidenceTailSignalNone, false, errors.New("streamgate: no blocking requirement for channel: " + channel)
}
if req.Mode() != FilterHoldModeFragmentGate {
return EvidenceEpoch{}, EvidenceTailSignalNone, false, fmt.Errorf("streamgate: channel %s is not fragment_gate mode (got %s)", channel, req.Mode())
}
cs, ok := t.channelState[channel]
if !ok {
return EvidenceEpoch{}, EvidenceTailSignalNone, false, nil
}
// Check if the specific fragment exists and is not already completed.
fragment, exists := cs.fragmentState[toolCallID]
if !exists || fragment.completed {
return EvidenceEpoch{}, EvidenceTailSignalNone, false, nil
}
// Mark as completed.
fragment.completed = true
// Check if a contiguous safe prefix was formed.
safeLen := cs.fragmentSafePrefixLength()
if safeLen == 0 {
return EvidenceEpoch{}, EvidenceTailSignalNone, true, nil
}
// Invalidate any previously prepared (but unconfirmed) epoch on this
// channel so that superseded safe-prefix snapshots cannot be re-prepared.
// Only affects the same channel; unrelated channel tokens remain valid.
t.markChannelEpochsInvalidated(channel)
delete(t.preparedByChannel, channel)
// Create an epoch for the newly completed safe prefix using the
// standardized ready-epoch path so the record inherits the same
// lifecycle invariants as threshold/trigger epochs.
firstSeq := cs.firstPendingSequence()
safeEndSeq := firstSeq + safeLen
epoch, _, err := t.nextReadyEpochRecord(
channel, FilterHoldModeFragmentGate, true, "fragment_completion_safe_prefix",
firstSeq, safeEndSeq,
)
if err != nil {
return EvidenceEpoch{}, EvidenceTailSignalNone, true, err
}
return epoch, EvidenceTailSignalTrigger, true, nil
}
// isFragmentCompleted returns true when the fragment for the given ID is completed.
func (cs *channelState) isFragmentCompleted(toolCallID string) bool {
fragment, exists := cs.fragmentState[toolCallID]
return exists && fragment.completed
}
// fragmentSafePrefixLength returns the index in pendingEntries up to which
// all fragments are completed and the prefix is contiguous. Returns 0 if no
// fragments are completed.
func (cs *channelState) fragmentSafePrefixLength() int {
if len(cs.pendingEntries) == 0 {
return 0
}
// Find the first incomplete fragment ID.
firstIncompleteID := ""
for _, entry := range cs.pendingEntries {
if entry.kind == EventKindToolCallFragment && !cs.isFragmentCompleted(entry.toolCallID) {
firstIncompleteID = entry.toolCallID
break
}
}
if firstIncompleteID == "" {
// All fragments are completed.
return len(cs.pendingEntries)
}
// Find the index of the first incomplete fragment.
for i, entry := range cs.pendingEntries {
if entry.toolCallID == firstIncompleteID {
return i
}
}
return 0
}
// PreparedRelease is an opaque token representing a prepared release that
// has not yet been confirmed. It is immutable and cannot be tampered with.
type PreparedRelease struct {
token string
releaseEvents []ReleaseEvent
}
// Token returns the prepared release token string.
func (pr PreparedRelease) Token() string { return pr.token }
// ReleaseEvents returns a defensive copy of the prepared release events.
func (pr PreparedRelease) ReleaseEvents() []ReleaseEvent {
if pr.releaseEvents == nil {
return nil
}
out := make([]ReleaseEvent, len(pr.releaseEvents))
copy(out, pr.releaseEvents)
return out
}
// ReleaseConfirmation carries the number of events successfully released
// during a confirm operation.
type ReleaseConfirmation struct {
ReleasedEvents int
}
// PrepareRelease creates a prepared release for the given epoch ID.
// Only epoch records that were created via a ready transition (threshold
// reached, trigger fired, fragment completion) carry release capability.
// Observation-only epochs (unsubscribed, overflow, observe-only, sub-threshold)
// have no record and cannot be prepared.
//
// PrepareRelease validates the epoch record exists and is not consumed or
// invalidated, generates a release snapshot from the epoch's fixed sequence
// range using unconfirmedSnapshot (which respects partial confirms), and
// creates an opaque token. It does NOT modify pending/look-behind/cursor
// state.
//
// PrepareRelease rejects: unknown/observation epochs, already consumed/
// invalidated epochs, overlapping prepared tokens for the same channel,
// and already-prepared epochs.
func (t *EvidenceTail) PrepareRelease(epochID uint64) (PreparedRelease, error) {
// Validate epoch record exists and is not consumed or invalidated.
record, ok := t.epochs[epochID]
if !ok {
return PreparedRelease{}, fmt.Errorf("streamgate: prepare release unknown or observation-only epoch %d", epochID)
}
if record.consumed || record.invalidated {
return PreparedRelease{}, fmt.Errorf("streamgate: prepare release epoch %d already consumed or invalidated", epochID)
}
// Overlapping prepared token on the same channel is rejected.
if t.preparedByChannel[record.channel] != "" {
return PreparedRelease{}, fmt.Errorf("streamgate: prepare release channel %s already has prepared token %s", record.channel, t.preparedByChannel[record.channel])
}
if record.prepared && record.token != "" {
return PreparedRelease{}, fmt.Errorf("streamgate: prepare release epoch %d already prepared with token %s", epochID, record.token)
}
channel := record.channel
// Get the release snapshot from the epoch's fixed sequence range.
// This uses monotonic sequence identity so it is safe after partial confirm.
channelCS := t.channelState[channel]
if channelCS == nil {
return PreparedRelease{}, errors.New("streamgate: prepare release no channel state for epoch channel")
}
releaseEvents := channelCS.unconfirmedSnapshot(record)
if len(releaseEvents) == 0 {
return PreparedRelease{}, fmt.Errorf("streamgate: prepare release epoch %d has no unconfirmed events", epochID)
}
// Generate an opaque, globally unique token with a monotonic nonce.
// This prevents stale token reuse when the same epoch is re-prepared
// after zero or partial confirm (the snapshot content could be identical).
t.tokenNonce++
token := fmt.Sprintf("prepared-%d-%d-%d", epochID, len(releaseEvents), t.tokenNonce)
// Store snapshot size for downstream ConfirmRelease validation.
record.snapshotSize = len(releaseEvents)
record.prepared = true
record.token = token
t.preparedByChannel[channel] = token
return PreparedRelease{
token: token,
releaseEvents: releaseEvents,
}, nil
}
// ConfirmRelease applies the confirmed number of released events to the
// committed look-behind and cursor. Only this method modifies pending/
// look-behind/cursor state. Zero, partial, and full confirmations are
// supported. Stale tokens and duplicate confirms are rejected.
//
// ConfirmRelease validates: token format and existence, that the token
// matches the epoch's prepared token, that ReleasedEvents does not exceed
// the prepared count, and that the epoch has not been invalidated.
//
// ConfirmRelease validates:
// - The token matches the epoch record and matches the current prepared
// token for the epoch's channel.
// - The epoch is not invalidated.
// - The pending prefix starting at the epoch's snapStartSeq matches
// the expected monotonic sequence.
// - ReleasedEvents does not exceed the prepared count.
func (t *EvidenceTail) ConfirmRelease(token string, confirmation ReleaseConfirmation) error {
if token == "" {
return errors.New("streamgate: confirm release token is required")
}
if !strings.HasPrefix(token, "prepared-") {
return errors.New("streamgate: confirm release invalid token format")
}
// Find the epoch record associated with this token.
var targetEpoch *epochRecord
for _, record := range t.epochs {
if record.token == token {
targetEpoch = record
break
}
}
if targetEpoch == nil {
return errors.New("streamgate: confirm release token not found")
}
if targetEpoch.consumed {
return fmt.Errorf("streamgate: confirm release epoch %d already consumed", targetEpoch.epochID)
}
// Validate ownership: token must match current prepared state and
// epoch must not be invalidated.
if err := t.validatePreparedOwnership(targetEpoch, token); err != nil {
return err
}
channel := targetEpoch.channel
cs, ok := t.channelState[channel]
if !ok {
return errors.New("streamgate: confirm release channel state not found")
}
n := confirmation.ReleasedEvents
if n < 0 {
return errors.New("streamgate: confirm release released events must be non-negative")
}
if n > targetEpoch.snapshotSize {
return fmt.Errorf("streamgate: confirm release %d exceeds prepared snapshot %d", n, targetEpoch.snapshotSize)
}
// Validate that the pending prefix matches the epoch's sequence range.
// This guards against a prepared token being confirmed after the pending
// prefix has diverged from the original snapshot (e.g., post-append
// contamination).
if !cs.pendingPrefixMatchesRange(n, targetEpoch.snapStartSeq) {
return errPreparedSnapshotMismatch
}
// Move confirmed events to look-behind.
confirmed := make([]NormalizedEvent, n)
for i := 0; i < n; i++ {
confirmed[i] = cloneNormalizedEvent(cs.pendingEntries[i].event)
}
cs.committedLookBehind = append(cs.committedLookBehind, confirmed...)
// Remove confirmed from pending.
cs.pendingEntries = cs.pendingEntries[n:]
// Adjust rune count.
removedRunes := 0
for i := 0; i < n; i++ {
removedRunes += runeCountForEvent(confirmed[i])
}
cs.pendingRunes -= removedRunes
// Update committed cursor.
cs.committedCursor += n
// Remove confirmed fragment states (if all their entries are confirmed).
// Use the absolute confirmed range so that non-zero sequence fragments
// clean up correctly: only entries already covered by this confirm are
// removed, and their absolute sequences are preserved for subsequent
// partial/full confirms.
confirmedEndSeq := targetEpoch.snapStartSeq + n
for id, fragment := range cs.fragmentState {
var remaining []int
for _, seq := range fragment.entries {
if seq >= confirmedEndSeq {
remaining = append(remaining, seq)
}
}
if len(remaining) == 0 {
delete(cs.fragmentState, id)
} else {
fragment.entries = remaining
}
}
// Trim look-behind to bounded size using the channel's effective evidence
// window (evidence_runes for rolling, maxBufferRunes otherwise).
cs.committedLookBehind = trimLookBehind(cs.committedLookBehind, cs.effectiveEvidenceRunes)
// Mark consumed when the full snapshot has been confirmed.
if n >= targetEpoch.snapshotSize {
targetEpoch.consumed = true
}
// Advance the epoch's unconfirmed start so that a subsequent re-prepare
// or partial/full confirm sees only the remaining suffix of the same epoch.
targetEpoch.snapStartSeq += n
targetEpoch.confirmedCount += n
// Clear prepared token regardless of completion.
targetEpoch.prepared = false
targetEpoch.token = ""
delete(t.preparedByChannel, channel)
return nil
}
// ResetForReplace clears all attempt-local state including pending entries,
// look-behind, fragment state, prepared tokens, and the token nonce. Used
// when a new attempt replaces the current one.
func (t *EvidenceTail) ResetForReplace() {
t.channelState = make(map[string]*channelState)
t.epochs = make(map[uint64]*epochRecord)
t.preparedByChannel = make(map[string]string)
t.tokenNonce = 0
}
// PrepareContinuation preserves committed look-behind and cursor while
// discarding pending entries and prepared tokens. Used when resuming after
// a recovery continuation.
func (t *EvidenceTail) PrepareContinuation() {
for _, cs := range t.channelState {
cs.pendingEntries = nil
cs.pendingRunes = 0
cs.fragmentState = make(map[string]*fragmentState)
// committedLookBehind, committedCursor are preserved.
}
t.markEpochsInvalidated()
t.preparedByChannel = make(map[string]string)
}
// DiscardPendingForTerminal discards all pending entries and prepared tokens
// without releasing anything. Used before terminal error or when the
// terminal gate overflows. No downstream write occurs.
func (t *EvidenceTail) DiscardPendingForTerminal() {
for _, cs := range t.channelState {
cs.pendingEntries = nil
cs.pendingRunes = 0
cs.fragmentState = make(map[string]*fragmentState)
// committedLookBehind and committedCursor are preserved.
}
t.markEpochsInvalidated()
t.preparedByChannel = make(map[string]string)
}
// markEpochsInvalidated marks all epoch records as invalidated and clears
// all token/prepared/snapshot state. This prevents any stale outstanding
// token from being confirmed after recovery/continuation/terminal discard.
func (t *EvidenceTail) markEpochsInvalidated() {
for _, record := range t.epochs {
record.invalidated = true
record.prepared = false
record.token = ""
record.snapshotSize = 0
}
}
// markChannelEpochsInvalidated marks all epoch records belonging to the given
// channel as invalidated and clears all token/prepared/snapshot state for
// those records. This prevents stale outstanding tokens on the same channel
// from being confirmed after fragment completion supersedes an older safe
// prefix snapshot. It does NOT affect epoch records on other channels.
func (t *EvidenceTail) markChannelEpochsInvalidated(channel string) {
for _, record := range t.epochs {
if record.channel == channel {
record.invalidated = true
record.prepared = false
record.token = ""
record.snapshotSize = 0
}
}
}
// validateEventUTF8 checks that text/reasoning/tool argument content in the
// event is valid UTF-8. Invalid UTF-8 is rejected with a stable error.
func validateEventUTF8(event NormalizedEvent) error {
var texts []string
switch event.Kind() {
case EventKindTextDelta:
text, err := event.AsTextDelta()
if err != nil {
return err
}
texts = append(texts, text)
case EventKindReasoningDelta:
text, err := event.AsReasoningDelta()
if err != nil {
return err
}
texts = append(texts, text)
case EventKindToolCallFragment:
tc, err := event.AsToolCallFragment()
if err != nil {
return err
}
texts = append(texts, tc.Arguments)
}
for _, text := range texts {
if text != "" && !utf8.ValidString(text) {
return fmt.Errorf("streamgate: event %s contains invalid UTF-8", event.Kind())
}
}
return nil
}
// runeCountForEvent returns the Unicode rune count for an event's text
// content. It operates on valid UTF-8 strings only.
func runeCountForEvent(event NormalizedEvent) int {
var total int
switch event.Kind() {
case EventKindTextDelta:
text, _ := event.AsTextDelta()
total += utf8.RuneCountInString(text)
case EventKindReasoningDelta:
text, _ := event.AsReasoningDelta()
total += utf8.RuneCountInString(text)
case EventKindToolCallFragment:
tc, _ := event.AsToolCallFragment()
total += utf8.RuneCountInString(tc.Arguments)
}
return total
}
// normalizedToReleaseEvent converts a NormalizedEvent to a ReleaseEvent.
// Only releasable kinds (text_delta, reasoning_delta, tool_call_fragment)
// are supported.
func normalizedToReleaseEvent(ev NormalizedEvent) (ReleaseEvent, error) {
switch ev.Kind() {
case EventKindTextDelta:
text, err := ev.AsTextDelta()
if err != nil {
return ReleaseEvent{}, err
}
return NewReleaseTextDeltaEvent(ev.Channel(), text, ev.Timestamp())
case EventKindReasoningDelta:
text, err := ev.AsReasoningDelta()
if err != nil {
return ReleaseEvent{}, err
}
return NewReleaseReasoningDeltaEvent(ev.Channel(), text, ev.Timestamp())
case EventKindToolCallFragment:
tc, err := ev.AsToolCallFragment()
if err != nil {
return ReleaseEvent{}, err
}
return NewReleaseToolCallFragmentEvent(ev.Channel(), tc.ID, tc.Name, tc.Arguments, ev.Timestamp())
default:
return ReleaseEvent{}, fmt.Errorf("streamgate: cannot release event kind %s", ev.Kind())
}
}
// CommittedCursor returns the total number of events confirmed released
// downstream for the given channel. Returns 0 if the channel has no state.
func (t *EvidenceTail) CommittedCursor(channel string) int {
cs, ok := t.channelState[channel]
if !ok {
return 0
}
return cs.committedCursor
}
// EffectiveLookBehind returns a defensive deep copy of the bounded look-behind
// events for the given channel. Returns nil if the channel has no state.
func (t *EvidenceTail) EffectiveLookBehind(channel string) []NormalizedEvent {
cs, ok := t.channelState[channel]
if !ok {
return nil
}
if len(cs.committedLookBehind) == 0 {
return nil
}
out := make([]NormalizedEvent, len(cs.committedLookBehind))
for i, ev := range cs.committedLookBehind {
out[i] = cloneNormalizedEvent(ev)
}
return out
}
// sliceEventAtRuneBoundary returns a new event with content sliced to at most
// maxRunes runes from the trailing edge. Returns an empty NormalizedEvent if
// the kind cannot be sliced (terminal, response_start, etc.).
func sliceEventAtRuneBoundary(ev NormalizedEvent, maxRunes int) (NormalizedEvent, error) {
var text string
switch ev.Kind() {
case EventKindTextDelta:
text, _ = ev.AsTextDelta()
case EventKindReasoningDelta:
text, _ = ev.AsReasoningDelta()
case EventKindToolCallFragment:
tc, _ := ev.AsToolCallFragment()
text = tc.Arguments
default:
return NormalizedEvent{}, nil
}
runes := []rune(text)
if len(runes) <= maxRunes {
return ev, nil
}
sliced := string(runes[len(runes)-maxRunes:])
switch ev.Kind() {
case EventKindTextDelta:
return NewTextDeltaEvent(ev.Channel(), sliced, ev.Timestamp())
case EventKindReasoningDelta:
return NewReasoningDeltaEvent(ev.Channel(), sliced, ev.Timestamp())
case EventKindToolCallFragment:
tc, _ := ev.AsToolCallFragment()
return NewToolCallFragmentEvent(ev.Channel(), tc.ID, tc.Name, sliced, ev.Timestamp())
}
return NormalizedEvent{}, nil
}
// trimLookBehind trims the look-behind to at most maxRunes worth of events,
// keeping the most recent events. Single oversized events are sliced at
// Unicode rune boundaries to preserve partial content as a defensive snapshot.
func trimLookBehind(events []NormalizedEvent, maxRunes int) []NormalizedEvent {
if len(events) == 0 || maxRunes <= 0 {
return events
}
// Calculate rune counts for each event.
runeCounts := make([]int, len(events))
totalRunes := 0
for i, ev := range events {
runeCounts[i] = runeCountForEvent(ev)
totalRunes += runeCounts[i]
}
if totalRunes <= maxRunes {
return events
}
// Keep from the end until we're within the limit.
var result []NormalizedEvent
remainingRunes := maxRunes
for i := len(events) - 1; i >= 0; i-- {
if remainingRunes >= runeCounts[i] {
result = append([]NormalizedEvent{events[i]}, result...)
remainingRunes -= runeCounts[i]
} else {
// Slice this event at rune boundary to preserve partial content.
sliced, err := sliceEventAtRuneBoundary(events[i], remainingRunes)
if err == nil && sliced.Kind() != "" {
result = append([]NormalizedEvent{sliced}, result...)
}
break
}
}
return result
}