// 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 }