// 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 FilterEnforcement resolved by the caller. The binding is // immutable to callers. type FilterHoldBinding struct { filterID StableToken requirement FilterHoldRequirement enforcement FilterEnforcement blocksRelease bool } // NewFilterHoldBinding creates a FilterHoldBinding with validation. The // returned value is immutable to callers. func NewFilterHoldBinding(filterID string, req FilterHoldRequirement, enforcement FilterEnforcement) (FilterHoldBinding, error) { f, err := NewStableTokenRequired("filterID", filterID) if err != nil { return FilterHoldBinding{}, err } if err := req.Validate(); err != nil { return FilterHoldBinding{}, err } if err := enforcement.Validate(); err != nil { return FilterHoldBinding{}, err } blocksRelease := (enforcement == FilterEnforcementBlocking && req.Mode() != FilterHoldModeNone) return FilterHoldBinding{ filterID: f, requirement: req, enforcement: enforcement, blocksRelease: blocksRelease, }, nil } // NewFilterHoldBindingFromResolvedFilter constructs a FilterHoldBinding from a ResolvedFilter. func NewFilterHoldBindingFromResolvedFilter(rf ResolvedFilter) (FilterHoldBinding, error) { return NewFilterHoldBinding(rf.FilterID(), rf.HoldRequirement(), rf.Enforcement()) } // 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 } // Enforcement returns the effective enforcement. func (b FilterHoldBinding) Enforcement() FilterEnforcement { return b.enforcement } // BlocksRelease returns whether this binding blocks channel release. func (b FilterHoldBinding) BlocksRelease() bool { return b.blocksRelease } // FilterApplicability captures typed readiness and subscription status for a single filter in an epoch. type FilterApplicability struct { filterID StableToken subscribedEventPresent bool triggerReady bool } // NewFilterApplicability creates a validated FilterApplicability. func NewFilterApplicability(filterID string, subscribedEventPresent, triggerReady bool) (FilterApplicability, error) { st, err := NewStableTokenRequired("filterID", filterID) if err != nil { return FilterApplicability{}, fmt.Errorf("streamgate: filter applicability id: %w", err) } return FilterApplicability{ filterID: st, subscribedEventPresent: subscribedEventPresent, triggerReady: triggerReady, }, nil } // FilterID returns the stable filter id. func (a FilterApplicability) FilterID() string { return a.filterID.value } // SubscribedEventPresent returns whether subscribed events were present for this filter in the epoch. func (a FilterApplicability) SubscribedEventPresent() bool { return a.subscribedEventPresent } // TriggerReady returns whether the filter trigger condition is satisfied in the epoch. func (a FilterApplicability) TriggerReady() bool { return a.triggerReady } // evidencePlan holds the compiled per-channel hold plan derived from bindings. type evidencePlan struct { bindingsByChannel map[string][]FilterHoldBinding allBindings map[string]FilterHoldBinding observeOnlyKinds map[string][]EventKind allKinds map[string][]EventKind blockingKinds map[string][]EventKind maxBufferRunes map[string]int rollingEvidenceRunes map[string]int } // compileEvidencePlan produces a compiled plan from a slice of bindings. func compileEvidencePlan(bindings []FilterHoldBinding) (evidencePlan, error) { plan := evidencePlan{ bindingsByChannel: make(map[string][]FilterHoldBinding), allBindings: make(map[string]FilterHoldBinding), observeOnlyKinds: make(map[string][]EventKind), allKinds: make(map[string][]EventKind), blockingKinds: make(map[string][]EventKind), maxBufferRunes: make(map[string]int), rollingEvidenceRunes: make(map[string]int), } for _, b := range bindings { fID := b.FilterID() if fID == "" { return evidencePlan{}, errors.New("streamgate: binding filter id is required") } if _, dup := plan.allBindings[fID]; dup { return evidencePlan{}, fmt.Errorf("streamgate: duplicate binding filter id: %s", fID) } req := b.requirement if err := req.Validate(); err != nil { return evidencePlan{}, fmt.Errorf("streamgate: binding requirement invalid: %w", err) } ch := req.Channel() plan.allBindings[fID] = b plan.bindingsByChannel[ch] = append(plan.bindingsByChannel[ch], b) plan.allKinds[ch] = mergeKinds(plan.allKinds[ch], req.SubscribedKinds()) if b.BlocksRelease() && req.IsBlocking() { plan.blockingKinds[ch] = mergeKinds(plan.blockingKinds[ch], req.SubscribedKinds()) plan.maxBufferRunes[ch] = minPositiveBound(plan.maxBufferRunes[ch], req.MaxBufferRunes()) if req.Mode() == FilterHoldModeRolling { plan.rollingEvidenceRunes[ch] = maxInt(plan.rollingEvidenceRunes[ch], req.EvidenceRunes()) } } else { plan.observeOnlyKinds[ch] = mergeKinds(plan.observeOnlyKinds[ch], req.SubscribedKinds()) } } // Sort bindings per channel by filter id ascending for ch := range plan.bindingsByChannel { sort.SliceStable(plan.bindingsByChannel[ch], func(i, j int) bool { return plan.bindingsByChannel[ch][i].FilterID() < plan.bindingsByChannel[ch][j].FilterID() }) } // Validate bounds per channel: max rolling evidence must be <= shared hard bound for ch := range plan.bindingsByChannel { maxBuf := plan.maxBufferRunes[ch] rollEv := plan.rollingEvidenceRunes[ch] if rollEv > 0 && maxBuf > 0 && rollEv > maxBuf { return evidencePlan{}, fmt.Errorf("streamgate: max rolling evidence %d > shared hard bound %d for channel %s", rollEv, maxBuf, ch) } } 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. 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] } // 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{ bindingsByChannel: make(map[string][]FilterHoldBinding), allBindings: make(map[string]FilterHoldBinding), observeOnlyKinds: make(map[string][]EventKind), allKinds: make(map[string][]EventKind), blockingKinds: make(map[string][]EventKind), maxBufferRunes: make(map[string]int), rollingEvidenceRunes: make(map[string]int), }}, nil } compiled, err := compileEvidencePlan(bindings) if err != nil { return EvidencePlan{}, err } return EvidencePlan{plan: compiled}, nil } // NewEvidencePlanFromResolvedFilters compiles an EvidencePlan from ResolvedFilters. func NewEvidencePlanFromResolvedFilters(resolved []ResolvedFilter) (EvidencePlan, error) { if len(resolved) == 0 { return NewEvidencePlan(nil) } seen := make(map[string]struct{}, len(resolved)) bindings := make([]FilterHoldBinding, len(resolved)) for i, rf := range resolved { fID := rf.FilterID() if fID == "" { return EvidencePlan{}, errors.New("streamgate: resolved filter id is required") } if _, dup := seen[fID]; dup { return EvidencePlan{}, fmt.Errorf("streamgate: duplicate resolved filter id: %s", fID) } seen[fID] = struct{}{} b, err := NewFilterHoldBindingFromResolvedFilter(rf) if err != nil { return EvidencePlan{}, fmt.Errorf("streamgate: build binding from resolved filter: %w", err) } bindings[i] = b } return NewEvidencePlan(bindings) } // BindingsForChannel returns a defensive copy of all bindings for the channel in stable filter id order. func (p EvidencePlan) BindingsForChannel(channel string) []FilterHoldBinding { bindings := p.plan.bindingsByChannel[channel] if len(bindings) == 0 { return nil } out := make([]FilterHoldBinding, len(bindings)) copy(out, bindings) return out } // Bindings returns a defensive copy of all bindings in stable filter id order. func (p EvidencePlan) Bindings() []FilterHoldBinding { var all []FilterHoldBinding for _, b := range p.plan.allBindings { all = append(all, b) } sort.SliceStable(all, func(i, j int) bool { return all[i].FilterID() < all[j].FilterID() }) return all } // HasModeForChannel returns true when channel has at least one binding with the given mode. func (p EvidencePlan) HasModeForChannel(channel string, mode FilterHoldMode) bool { bindings := p.plan.bindingsByChannel[channel] for _, b := range bindings { if b.Requirement().Mode() == mode { return true } } return false } // IsTerminalTriggerForChannel returns true when eventKind is a configured terminal trigger for any binding on channel. func (p EvidencePlan) IsTerminalTriggerForChannel(channel string, kind EventKind) bool { bindings := p.plan.bindingsByChannel[channel] for _, b := range bindings { if b.BlocksRelease() && b.Requirement().Mode() == FilterHoldModeTerminalGate { if b.Requirement().TriggerKind() == kind { return true } } } return false } // BlockingRequirementFor returns a representative storage envelope requirement for the channel. func (p EvidencePlan) BlockingRequirementFor(channel string) (FilterHoldRequirement, bool) { bindings := p.plan.bindingsByChannel[channel] if len(bindings) == 0 { return FilterHoldRequirement{}, false } var strongestMode FilterHoldMode = FilterHoldModeNone var triggerKind EventKind for _, b := range bindings { if !b.BlocksRelease() || !b.Requirement().IsBlocking() { continue } m := b.Requirement().Mode() if strongerMode(m, strongestMode) { strongestMode = m triggerKind = b.Requirement().TriggerKind() } } if strongestMode == FilterHoldModeNone { return FilterHoldRequirement{}, false } maxBuf := p.plan.maxBufferRunes[channel] rollEv := p.plan.rollingEvidenceRunes[channel] blockingKinds := p.plan.blockingKinds[channel] var req FilterHoldRequirement var err error switch strongestMode { case FilterHoldModeRolling: if rollEv <= 0 { rollEv = defaultEvidenceRunes } if maxBuf <= 0 { maxBuf = defaultMaxBufferRunes } if maxBuf < rollEv { maxBuf = rollEv } req, err = NewFilterHoldRequirementRollingWithMaxBuffer(channel, blockingKinds, rollEv, maxBuf) case FilterHoldModeTerminalGate: if maxBuf <= 0 { maxBuf = defaultMaxBufferRunes } req, err = NewFilterHoldRequirementTerminalGateWithMaxBuffer(channel, blockingKinds, triggerKind, maxBuf) case FilterHoldModeFragmentGate: if maxBuf <= 0 { maxBuf = defaultMaxBufferRunes } req, err = NewFilterHoldRequirementFragmentGateWithMaxBuffer(channel, blockingKinds, triggerKind, maxBuf) } if err != nil { return FilterHoldRequirement{}, false } return req, true } // HasBlockingRequirement returns true when the given channel has at least one // blocking requirement in the plan. func (p EvidencePlan) HasBlockingRequirement(channel string) bool { bindings := p.plan.bindingsByChannel[channel] for _, b := range bindings { if b.BlocksRelease() && b.Requirement().IsBlocking() { return true } } return false } // BlockingRequirement returns the representative blocking requirement for the // channel, or an error if no blocking requirement exists. func (p EvidencePlan) BlockingRequirement(channel string) (FilterHoldRequirement, error) { req, ok := p.BlockingRequirementFor(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 } // AllBindings returns a defensive copy of all bindings in stable filter ID order. func (p EvidencePlan) AllBindings() []FilterHoldBinding { if p.plan.allBindings == nil { return nil } out := make([]FilterHoldBinding, 0, len(p.plan.allBindings)) for _, b := range p.plan.allBindings { out = append(out, b) } sort.SliceStable(out, func(i, j int) bool { return out[i].FilterID() < out[j].FilterID() }) return out } func (p EvidencePlan) bindingIDs() []string { bindings := p.AllBindings() ids := make([]string, len(bindings)) for i, b := range bindings { ids[i] = b.FilterID() } return ids } // MaxBufferRunes returns the compiled max buffer runes for the channel. func (p EvidencePlan) MaxBufferRunes(channel string) int { return p.plan.maxBufferRunes[channel] } // RollingEvidenceRunes returns the compiled rolling evidence runes for the channel. func (p EvidencePlan) RollingEvidenceRunes(channel string) int { return p.plan.rollingEvidenceRunes[channel] } // BindEpochFilters evaluates epoch applicability and binds resolved filters to EpochFilters in stable filter ID order. func (p EvidencePlan) BindEpochFilters(epoch EvidenceEpoch, resolvedFilters []ResolvedFilter) ([]EpochFilter, error) { if epoch.ID() == 0 { return nil, errors.New("streamgate: epoch id must be positive") } planIDs := p.bindingIDs() if len(planIDs) == 0 && len(resolvedFilters) == 0 { return nil, nil } resolvedByID, err := indexResolvedFilters(resolvedFilters) if err != nil { return nil, err } appsByID, err := indexApplicabilities(epoch.Applicabilities()) if err != nil { return nil, err } if err := requireExactFilterIDs(planIDs, resolvedByID, appsByID); err != nil { return nil, err } out := make([]EpochFilter, 0, len(planIDs)) for _, id := range planIDs { rf := resolvedByID[id] app := appsByID[id] ef, err := rf.BindEpoch(epoch.ID(), app) if err != nil { return nil, fmt.Errorf("streamgate: bind epoch filter %s: %w", id, err) } out = append(out, ef) } return out, nil } func indexResolvedFilters(filters []ResolvedFilter) (map[string]ResolvedFilter, error) { out := make(map[string]ResolvedFilter, len(filters)) for _, rf := range filters { fID := rf.FilterID() if fID == "" { return nil, errors.New("streamgate: resolved filter id is required") } if _, dup := out[fID]; dup { return nil, fmt.Errorf("streamgate: duplicate resolved filter id in bind epoch: %s", fID) } out[fID] = rf } return out, nil } func indexApplicabilities(apps []FilterApplicability) (map[string]FilterApplicability, error) { out := make(map[string]FilterApplicability, len(apps)) for _, app := range apps { fID := app.FilterID() if fID == "" { return nil, errors.New("streamgate: applicability filter id is required") } if _, dup := out[fID]; dup { return nil, fmt.Errorf("streamgate: duplicate applicability filter id in bind epoch: %s", fID) } out[fID] = app } return out, nil } func requireExactFilterIDs(planIDs []string, resolvedByID map[string]ResolvedFilter, appsByID map[string]FilterApplicability) error { if len(resolvedByID) != len(planIDs) { return fmt.Errorf("streamgate: resolved filter count mismatch: got %d, expected %d", len(resolvedByID), len(planIDs)) } if len(appsByID) != len(planIDs) { return fmt.Errorf("streamgate: applicability count mismatch: got %d, expected %d", len(appsByID), len(planIDs)) } for _, id := range planIDs { if _, ok := resolvedByID[id]; !ok { return fmt.Errorf("streamgate: missing resolved filter for id %s", id) } if _, ok := appsByID[id]; !ok { return fmt.Errorf("streamgate: missing applicability for id %s", id) } } return nil } // 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 and filter applicabilities. type EvidenceEpoch struct { id uint64 channel string mode FilterHoldMode triggered bool reason string applicabilities map[string]FilterApplicability } // NewEvidenceEpoch creates an EvidenceEpoch with the given parameters. func NewEvidenceEpoch(id uint64, channel string, mode FilterHoldMode, triggered bool, reason string) (EvidenceEpoch, error) { return NewEvidenceEpochWithApplicabilities(id, channel, mode, triggered, reason, nil) } // NewEvidenceEpochWithApplicabilities creates an EvidenceEpoch carrying filter applicabilities. func NewEvidenceEpochWithApplicabilities( id uint64, channel string, mode FilterHoldMode, triggered bool, reason string, apps []FilterApplicability, ) (EvidenceEpoch, error) { if channel == "" { return EvidenceEpoch{}, errors.New("streamgate: epoch channel is required") } if err := mode.Validate(); err != nil { return EvidenceEpoch{}, err } appMap := make(map[string]FilterApplicability, len(apps)) for _, app := range apps { fID := app.FilterID() if fID == "" { return EvidenceEpoch{}, errors.New("streamgate: epoch applicability filter id is required") } if _, dup := appMap[fID]; dup { return EvidenceEpoch{}, fmt.Errorf("streamgate: duplicate epoch applicability filter id: %s", fID) } appMap[fID] = app } return EvidenceEpoch{ id: id, channel: channel, mode: mode, triggered: triggered, reason: reason, applicabilities: appMap, }, 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 } // ApplicabilityFor returns the applicability for the filter ID, if present. func (e EvidenceEpoch) ApplicabilityFor(filterID string) (FilterApplicability, bool) { if e.applicabilities == nil { return FilterApplicability{}, false } app, ok := e.applicabilities[filterID] return app, ok } // Applicabilities returns a defensive copy of all applicabilities in stable filter ID order. func (e EvidenceEpoch) Applicabilities() []FilterApplicability { if e.applicabilities == nil { return nil } out := make([]FilterApplicability, 0, len(e.applicabilities)) for _, app := range e.applicabilities { out = append(out, app) } sort.Slice(out, func(i, j int) bool { return out[i].filterID.value < out[j].filterID.value }) return out } // 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. Prepared // replacements share this slot so a replacement and a release can never be // outstanding on the same channel at once. preparedByChannel map[string]string // replacements tracks in-flight prepared replacements by token. Each record // binds the replacement payload to the exact epoch, channel, and pending // sequence range it settles. replacements map[string]*replacementRecord // 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 } // replacementRecord tracks a prepared replacement through its lifecycle. It // anchors the substituted payload to one epoch, that epoch's channel, and the // immutable pending-entry sequence range the epoch captured, so confirm can // settle exactly that range and nothing else. type replacementRecord struct { token string epochID uint64 channel string snapStartSeq int // first pending-entry sequence being replaced snapEndSeq int // last pending-entry sequence (exclusive) being replaced events []NormalizedEvent // normalized replacement payload snapshot } // 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), replacements: make(map[string]*replacementRecord), tokenNonce: 0, }, nil } // validate checks the plan is consistent. func (p EvidencePlan) validate() error { for fID, b := range p.plan.allBindings { if err := b.Requirement().Validate(); err != nil { return fmt.Errorf("streamgate: compiled plan filter %s: %v", fID, 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) { return t.nextObservationEpochWithApps(channel, mode, triggered, reason, nil) } func (t *EvidenceTail) nextObservationEpochWithApps(channel string, mode FilterHoldMode, triggered bool, reason string, apps []FilterApplicability) (EvidenceEpoch, error) { t.epochCounter++ epoch, err := NewEvidenceEpochWithApplicabilities(t.epochCounter, channel, mode, triggered, reason, apps) if err != nil { return EvidenceEpoch{}, err } return epoch, nil } func (t *EvidenceTail) nextReadyEpochRecord(channel string, mode FilterHoldMode, triggered bool, reason string, firstSeq, lastSeqExclusive int) (EvidenceEpoch, *epochRecord, error) { return t.nextReadyEpochRecordWithApps(channel, mode, triggered, reason, firstSeq, lastSeqExclusive, nil) } func (t *EvidenceTail) nextReadyEpochRecordWithApps(channel string, mode FilterHoldMode, triggered bool, reason string, firstSeq, lastSeqExclusive int, apps []FilterApplicability) (EvidenceEpoch, *epochRecord, error) { t.epochCounter++ epoch, err := NewEvidenceEpochWithApplicabilities(t.epochCounter, channel, mode, triggered, reason, apps) 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 } // computeApplicabilities derives typed readiness and subscription status for each filter in the plan. type applicabilityInput struct { event *NormalizedEvent isTerminalTrigger bool isFragmentComplete bool isHardBound bool toolCallID string eventBuffered bool } type pendingStats struct { present bool runes int } func subscribedPendingStats(cs *channelState, subscribedKinds []EventKind) pendingStats { var stats pendingStats if cs == nil || len(cs.pendingEntries) == 0 { return stats } for _, entry := range cs.pendingEntries { if isKindSubscribed(entry.kind, subscribedKinds) { stats.present = true stats.runes += entry.runes } } return stats } func deriveTriggerReady( b FilterHoldBinding, req FilterHoldRequirement, stats pendingStats, cs *channelState, input applicabilityInput, ) bool { if input.isHardBound { if b.BlocksRelease() && req.IsBlocking() { return true } return false } if input.isTerminalTrigger { switch req.Mode() { case FilterHoldModeTerminalGate: return input.event != nil && (input.event.Kind() == req.TriggerKind() || input.event.Kind() == EventKindTerminal || input.event.Kind() == EventKindProviderError) case FilterHoldModeRolling, FilterHoldModeFragmentGate, FilterHoldModeNone: return true } return false } if input.isFragmentComplete { switch req.Mode() { case FilterHoldModeFragmentGate: return cs != nil && cs.isFragmentCompleted(input.toolCallID) case FilterHoldModeRolling: return stats.runes >= req.EvidenceRunes() } return false } switch req.Mode() { case FilterHoldModeRolling: return stats.runes >= req.EvidenceRunes() case FilterHoldModeNone: return input.event != nil && input.event.Kind() == EventKindProviderError && isKindSubscribed(EventKindProviderError, req.SubscribedKinds()) } return false } func (t *EvidenceTail) computeApplicabilities( channel string, event *NormalizedEvent, isTerminalTrigger bool, isFragmentComplete bool, isHardBound bool, toolCallID string, eventBuffered bool, ) ([]FilterApplicability, error) { allBindings := t.plan.AllBindings() if len(allBindings) == 0 { return nil, nil } cs := t.channelState[channel] input := applicabilityInput{ event: event, isTerminalTrigger: isTerminalTrigger, isFragmentComplete: isFragmentComplete, isHardBound: isHardBound, toolCallID: toolCallID, eventBuffered: eventBuffered, } apps := make([]FilterApplicability, 0, len(allBindings)) for _, b := range allBindings { fID := b.FilterID() req := b.Requirement() if req.Channel() != channel { app, err := NewFilterApplicability(fID, false, false) if err != nil { return nil, err } apps = append(apps, app) continue } stats := subscribedPendingStats(cs, req.SubscribedKinds()) if input.event != nil && !input.eventBuffered && isKindSubscribed(input.event.Kind(), req.SubscribedKinds()) { stats.present = true stats.runes += runeCountForEvent(*input.event) } subscribed := stats.present ready := deriveTriggerReady(b, req, stats, cs, input) app, err := NewFilterApplicability(fID, subscribed, ready) if err != nil { return nil, err } apps = append(apps, app) } return apps, nil } 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 } func (t *EvidenceTail) validatePreparedOwnership(record *epochRecord, token string) error { if record.invalidated { return errStalePreparedRelease } if t.preparedByChannel[record.channel] != token { return errStalePreparedRelease } return nil } 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 } // rangeEntryCount returns how many pending entries still fall inside the // half-open sequence range captured by an epoch snapshot. func (cs *channelState) rangeEntryCount(startSeq, endSeqExclusive int) int { count := 0 for _, entry := range cs.pendingEntries { if entry.sequence >= startSeq && entry.sequence < endSeqExclusive { count++ } } return count } // dropFragmentEntriesBelow removes fragment bookkeeping for pending entries // whose sequence is already settled. Absolute sequences are preserved for the // remaining entries so subsequent partial/full settlements stay correct. func (cs *channelState) dropFragmentEntriesBelow(settledEndSeq int) { for id, fragment := range cs.fragmentState { var remaining []int for _, seq := range fragment.entries { if seq >= settledEndSeq { remaining = append(remaining, seq) } } if len(remaining) == 0 { delete(cs.fragmentState, id) } else { fragment.entries = remaining } } } func (t *EvidenceTail) getOrCreateChannel(channel string, maxBuffer int, evidenceRunes int) (*channelState, error) { cs, ok := t.channelState[channel] if !ok { 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 } 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) if err := validateEventUTF8(event); err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } if !hasBlocking { apps, err := t.computeApplicabilities(channel, &event, false, false, false, "", false) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } epoch, err := t.nextObservationEpochWithApps(channel, FilterHoldModeNone, false, "pass_through_no_requirement", apps) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } return epoch, EvidenceTailSignalReady, nil } if event.Kind() == EventKindTerminal || event.Kind() == EventKindProviderError || t.plan.IsTerminalTriggerForChannel(channel, event.Kind()) { cs, err := t.getOrCreateChannel(channel, t.plan.MaxBufferRunes(channel), t.plan.RollingEvidenceRunes(channel)) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } safeEnd := cs.nextSequence firstSeq := cs.firstPendingSequence() apps, err := t.computeApplicabilities(channel, &event, true, false, false, "", false) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } epoch, _, err := t.nextReadyEpochRecordWithApps( channel, req.Mode(), true, "configured_trigger", firstSeq, safeEnd, apps, ) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } return epoch, EvidenceTailSignalTrigger, nil } blockingKinds := t.plan.BlockingKinds(channel) observeKinds := t.plan.ObserveKinds(channel) if !isKindSubscribed(event.Kind(), blockingKinds) { reason := "event_not_subscribed" if isKindSubscribed(event.Kind(), observeKinds) { reason = "observe_only_pass_through" } apps, err := t.computeApplicabilities(channel, &event, false, false, false, "", false) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } epoch, err := t.nextObservationEpochWithApps(channel, req.Mode(), false, reason, apps) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } return epoch, EvidenceTailSignalNone, nil } cs, err := t.getOrCreateChannel(channel, t.plan.MaxBufferRunes(channel), t.plan.RollingEvidenceRunes(channel)) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } runesAdded := runeCountForEvent(event) newPendingRunes := cs.pendingRunes + runesAdded if newPendingRunes > cs.maxBufferRunes { apps, err := t.computeApplicabilities(channel, &event, false, false, true, "", false) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } epoch, err := t.nextObservationEpochWithApps(channel, req.Mode(), false, "overflow_beyond_buffer_limit", apps) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } return epoch, EvidenceTailSignalBufferOverflow, nil } seq := cs.nextSequence cs.nextSequence++ entry := pendingEntry{ event: cloneNormalizedEvent(event), runes: runesAdded, kind: event.Kind(), sequence: seq, } if event.Kind() == EventKindToolCallFragment { tc, err := event.AsToolCallFragment() if err == nil { entry.toolCallID = tc.ID 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 { return EvidenceEpoch{}, EvidenceTailSignalBufferOverflow, nil } fragment.entries = append(fragment.entries, entry.sequence) fragment.runes = newFragmentRunes } } cs.pendingEntries = append(cs.pendingEntries, entry) cs.pendingRunes = newPendingRunes apps, err := t.computeApplicabilities(channel, &event, false, false, false, "", true) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } anyReady := false hasRollingReady := false for _, app := range apps { if app.TriggerReady() { anyReady = true for _, b := range t.plan.BindingsForChannel(channel) { if b.FilterID() == app.FilterID() && b.Requirement().Mode() == FilterHoldModeRolling { hasRollingReady = true } } } } if anyReady { safeEnd := cs.nextSequence firstSeq := cs.firstPendingSequence() epoch, _, err := t.nextReadyEpochRecordWithApps( channel, req.Mode(), true, "threshold_or_trigger_reached", firstSeq, safeEnd, apps, ) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } sig := EvidenceTailSignalTrigger if hasRollingReady { sig = EvidenceTailSignalThreshold } return epoch, sig, nil } epoch, err := t.nextObservationEpochWithApps(channel, req.Mode(), false, "sub_threshold_accumulating", apps) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, err } return epoch, EvidenceTailSignalNone, nil } func (cs *channelState) firstPendingSequence() int { if len(cs.pendingEntries) == 0 { return cs.nextSequence } return cs.pendingEntries[0].sequence } 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") } if !t.plan.HasModeForChannel(channel, FilterHoldModeFragmentGate) { return EvidenceEpoch{}, EvidenceTailSignalNone, false, fmt.Errorf("streamgate: channel %s is not fragment_gate mode", channel) } cs, ok := t.channelState[channel] if !ok { return EvidenceEpoch{}, EvidenceTailSignalNone, false, nil } fragment, exists := cs.fragmentState[toolCallID] if !exists || fragment.completed { return EvidenceEpoch{}, EvidenceTailSignalNone, false, nil } fragment.completed = true safeLen := cs.fragmentSafePrefixLength() if safeLen == 0 { return EvidenceEpoch{}, EvidenceTailSignalNone, true, nil } t.markChannelEpochsInvalidated(channel) delete(t.preparedByChannel, channel) t.dropChannelReplacements(channel) firstSeq := cs.firstPendingSequence() safeEndSeq := firstSeq + safeLen apps, err := t.computeApplicabilities(channel, nil, false, true, false, toolCallID, false) if err != nil { return EvidenceEpoch{}, EvidenceTailSignalNone, true, err } epoch, _, err := t.nextReadyEpochRecordWithApps( channel, FilterHoldModeFragmentGate, true, "fragment_completion_safe_prefix", firstSeq, safeEndSeq, apps, ) 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. // HasUnconfirmedEvents returns true if the specified epoch has unconfirmed release payload events. func (t *EvidenceTail) HasUnconfirmedEvents(epochID uint64) bool { if t == nil || t.epochs == nil { return false } record, ok := t.epochs[epochID] if !ok || record.consumed || record.invalidated { return false } channelCS := t.channelState[record.channel] if channelCS == nil { return false } return len(channelCS.unconfirmedSnapshot(record)) > 0 } 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. cs.dropFragmentEntriesBelow(targetEpoch.snapStartSeq + n) // 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 } // preparedReplacement is an opaque token representing a prepared replacement // that has not yet been confirmed. It is immutable and cannot be tampered // with, and it is only meaningful for the epoch it was prepared against. type preparedReplacement struct { token string releaseEvents []ReleaseEvent } // Token returns the prepared replacement token string. func (pr preparedReplacement) Token() string { return pr.token } // ReleaseEvents returns a defensive copy of the replacement release events. func (pr preparedReplacement) ReleaseEvents() []ReleaseEvent { if pr.releaseEvents == nil { return nil } out := make([]ReleaseEvent, len(pr.releaseEvents)) copy(out, pr.releaseEvents) return out } // prepareReplacement validates a replacement payload against the epoch it // substitutes and reserves the epoch's channel, without mutating pending, // look-behind, or cursor state. Every failure mode is checked here so a caller // can hand the payload to the sink knowing the tail is still untouched: // the epoch must exist as a releasable record that is neither consumed nor // invalidated, its channel must not already carry an outstanding prepared // token, the captured sequence range must still match the pending prefix, and // the payload must be non-empty with every event releasable and on the epoch's // own channel. func (t *EvidenceTail) prepareReplacement(epochID uint64, events []NormalizedEvent) (preparedReplacement, error) { if len(events) == 0 { return preparedReplacement{}, errors.New("streamgate: prepare replacement requires at least one event") } record, ok := t.epochs[epochID] if !ok { return preparedReplacement{}, fmt.Errorf("streamgate: prepare replacement unknown or observation-only epoch %d", epochID) } if record.consumed || record.invalidated { return preparedReplacement{}, fmt.Errorf("streamgate: prepare replacement epoch %d already consumed or invalidated", epochID) } if t.preparedByChannel[record.channel] != "" { return preparedReplacement{}, fmt.Errorf("streamgate: prepare replacement channel %s already has prepared token %s", record.channel, t.preparedByChannel[record.channel]) } if record.snapEndSeq < record.snapStartSeq { return preparedReplacement{}, fmt.Errorf("streamgate: prepare replacement epoch %d has an inverted sequence range", epochID) } channel := record.channel cs := t.channelState[channel] if cs == nil { return preparedReplacement{}, errors.New("streamgate: prepare replacement no channel state for epoch channel") } expectedCount := record.snapEndSeq - record.snapStartSeq targetCount := cs.rangeEntryCount(record.snapStartSeq, record.snapEndSeq) if targetCount != expectedCount { return preparedReplacement{}, errPreparedSnapshotMismatch } if expectedCount > 0 && !cs.pendingPrefixMatchesRange(expectedCount, record.snapStartSeq) { return preparedReplacement{}, errPreparedSnapshotMismatch } releaseEvents := make([]ReleaseEvent, len(events)) payload := make([]NormalizedEvent, len(events)) for i, ev := range events { if err := ev.Validate(); err != nil { return preparedReplacement{}, fmt.Errorf("streamgate: prepare replacement event %d: %v", i, err) } if ev.Channel() != channel { return preparedReplacement{}, fmt.Errorf("streamgate: prepare replacement event %d channel %s does not match epoch channel %s", i, ev.Channel(), channel) } relEv, err := normalizedToReleaseEvent(ev) if err != nil { return preparedReplacement{}, err } releaseEvents[i] = relEv payload[i] = cloneNormalizedEvent(ev) } // The nonce keeps the token globally unique so a stale replacement token // can never confirm a later replacement of the same epoch. The distinct // prefix keeps replacement tokens out of ConfirmRelease. t.tokenNonce++ token := fmt.Sprintf("replacement-%d-%d-%d", epochID, len(releaseEvents), t.tokenNonce) t.replacements[token] = &replacementRecord{ token: token, epochID: epochID, channel: channel, snapStartSeq: record.snapStartSeq, snapEndSeq: record.snapEndSeq, events: payload, } t.preparedByChannel[channel] = token return preparedReplacement{ token: token, releaseEvents: releaseEvents, }, nil } // confirmReplacement settles a prepared replacement against the sink progress // the boundary actually reported. The whole target range is consumed as // replaced regardless of progress - the original events were superseded and can // never reach the sink - but only the replacement prefix the sink accepted // becomes committed look-behind and advances the cursor, so zero, partial, and // full progress all leave the committed view equal to downstream reality. // // Only the target epoch's own range is touched: post-snapshot entries on the // same channel stay pending, unrelated channels keep their pending entries, // prepared tokens, and epoch records, and only same-channel epochs whose range // overlaps the consumed original range are invalidated. func (t *EvidenceTail) confirmReplacement(token string, confirmation ReleaseConfirmation) error { if token == "" { return errors.New("streamgate: confirm replacement token is required") } if !strings.HasPrefix(token, "replacement-") { return errors.New("streamgate: confirm replacement invalid token format") } replacement, ok := t.replacements[token] if !ok { return errStalePreparedRelease } record, ok := t.epochs[replacement.epochID] if !ok || record.consumed || record.invalidated { delete(t.replacements, token) return errStalePreparedRelease } if t.preparedByChannel[replacement.channel] != token { return errStalePreparedRelease } cs := t.channelState[replacement.channel] if cs == nil { return errors.New("streamgate: confirm replacement channel state not found") } n := confirmation.ReleasedEvents if n < 0 { return errors.New("streamgate: confirm replacement released events must be non-negative") } if n > len(replacement.events) { return fmt.Errorf("streamgate: confirm replacement %d exceeds prepared replacement %d", n, len(replacement.events)) } expectedCount := replacement.snapEndSeq - replacement.snapStartSeq targetCount := cs.rangeEntryCount(replacement.snapStartSeq, replacement.snapEndSeq) if targetCount != expectedCount { return errPreparedSnapshotMismatch } if expectedCount > 0 && !cs.pendingPrefixMatchesRange(expectedCount, replacement.snapStartSeq) { return errPreparedSnapshotMismatch } // Consume the superseded original range without any downstream write. if targetCount > 0 { removedRunes := 0 for i := 0; i < targetCount; i++ { removedRunes += cs.pendingEntries[i].runes } cs.pendingEntries = cs.pendingEntries[targetCount:] cs.pendingRunes -= removedRunes cs.dropFragmentEntriesBelow(replacement.snapEndSeq) } // Only the accepted replacement prefix becomes committed evidence. if n > 0 { released := make([]NormalizedEvent, n) for i := 0; i < n; i++ { released[i] = cloneNormalizedEvent(replacement.events[i]) } cs.committedLookBehind = append(cs.committedLookBehind, released...) cs.committedCursor += n cs.committedLookBehind = trimLookBehind(cs.committedLookBehind, cs.effectiveEvidenceRunes) } record.consumed = true record.prepared = false record.token = "" record.snapshotSize = 0 record.snapStartSeq = replacement.snapEndSeq t.invalidateOverlappingEpochs(replacement) delete(t.preparedByChannel, replacement.channel) delete(t.replacements, token) return nil } // invalidateOverlappingEpochs invalidates the still-open epoch records whose // captured range on the replaced channel overlaps the consumed original range, // so no stale token can confirm content that a replacement already superseded. // Records on other channels, already settled records, and same-channel records // covering only post-snapshot entries are left untouched. func (t *EvidenceTail) invalidateOverlappingEpochs(replacement *replacementRecord) { for _, record := range t.epochs { if record.epochID == replacement.epochID || record.channel != replacement.channel { continue } if record.consumed || record.invalidated { continue } if record.snapStartSeq >= replacement.snapEndSeq || record.snapEndSeq <= replacement.snapStartSeq { continue } record.invalidated = true record.prepared = false record.token = "" record.snapshotSize = 0 } } // 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.replacements = make(map[string]*replacementRecord) 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) t.replacements = make(map[string]*replacementRecord) } // 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) t.replacements = make(map[string]*replacementRecord) } // 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 } } // dropChannelReplacements removes the prepared replacement records belonging to // the given channel. Records on other channels are left untouched. func (t *EvidenceTail) dropChannelReplacements(channel string) { for token, replacement := range t.replacements { if replacement.channel == channel { delete(t.replacements, token) } } } // 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 }