--- # Authoritative formal design for realtime machine M3: response coordination. # # Companion to: # - docs/design/realtime-state-machines.md (the map + invariants) # - core/http/endpoints/openai/respcoord (the Go implementation) # # The Go MBT adapter maps each action below onto respcoord.Coordinator.Apply # and the StateGetter onto respcoord.Coordinator.State, so this spec is the # source of truth the implementation is checked against. # # NOTE: FizzBee is pre-1.0. Validate the exact syntax/CLI against the version # pinned in formal-verification/README.md before trusting the gate. deadlock_detection: false --- # Bound the number of responses so the state space is finite. MAX_RESPONSES = 4 role Session: action Init: self.live = 0 # number of live responses -- MUST stay in {0,1} self.registered = 0 # id of the active response (0 = none) self.next_id = 0 self.torn = 0 # session torn down (Terminated) -- absorbing # startResponse as ONE indivisible transition -- this is the single-writer # actor guarantee. Superseding an active response emits its cancelled # terminal (live -= 1) BEFORE spawning the replacement (live += 1), so the # net live count never exceeds 1. # # To reproduce the LEGACY dual-writer race from Part 2 of the design doc, # change `atomic func` to `serial func`: the checker then interleaves two # callers between the cancel and the spawn and reports AtMostOneLive # violated -- exactly the bug TestLegacyMechanismCanDoubleStart pins in Go. atomic func start(): if self.registered != 0: self.live -= 1 # cancel + cancelled-terminal for the old self.registered = 0 self.next_id += 1 self.live += 1 # spawn + register the replacement self.registered = self.next_id # client read-loop path: response.create / manual input_audio_buffer.commit. # Rejected once torn (no response starts after teardown). atomic action StartFromClient: require self.next_id < MAX_RESPONSES require self.torn == 0 self.start() # VAD goroutine path: end-of-speech commit / barge-in. Rejected once torn. atomic action StartFromVad: require self.next_id < MAX_RESPONSES require self.torn == 0 self.start() # a response reaches its own terminal (response.done completed) atomic action FinishCurrent: if self.registered != 0: self.live -= 1 self.registered = 0 # explicit response.cancel with nothing newer queued atomic action CancelReq: if self.registered != 0: self.live -= 1 self.registered = 0 # session teardown (M1 parent): cancel any in-flight response and go to the # absorbing Terminated state, after which no response can start. This is what # lets the connection's teardown guarantee no response outlives the session. atomic action Shutdown: if self.registered != 0: self.live -= 1 self.registered = 0 self.torn = 1 action Init: s = Session() # SAFETY: at most one live response at any instant (Part 4, invariant #1). always assertion AtMostOneLive: return s.live >= 0 and s.live <= 1